Apex Triggers

Get Started with Apex Triggers

Write a trigger for a Salesforce object. Use trigger context variables. Call a class method from a trigger. Use the sObject addError() method in a trigger to restrict save operations.

Apex Trigger basics

  • Use Apex Triggers to perform custom actions to records before or after events like insertions, updates, or deletions
    • Generally, they are used to modify related records or restrict certain operations from happening
    • Triggers can do anything you can do in Apex
  • Best practice is to use triggers to do tasks that can’t be done using point-and-click tools in Salesforce.
    • Triggers should not be used if you just need to validate a field value or update a field on a record - use validation rules/process builders/flows instead.
  • Triggers are active by default when created.

Trigger Syntax

  • Trigger syntax is different than a class definition’s syntax.
    • Possible trigger events:
      • before insert / after insert
      • before update / after update
      • before delete / after delete
      • after undelete
    • Include these in a comma-separated list in place of trigger_events, below
trigger TriggerName on ObjectName (trigger_events) {
   code_block
}

Example Simple Trigger

  • Create a new trigger via: Dev Console > File > New > Apex Trigger
    • Enter HelloWorldTrigger for the trigger name, select Account for sObject.
trigger HelloWorldTrigger on Account (before insert) {
	System.debug('Hello World!');
}
  • Try out the trigger by creating an account via: Dev Console > Debug > Open Execute Anonymous Window
Account a = new Account(Name='Test Trigger');
insert a;

  • Two types of triggers:
    • Before Triggers: Update or validate record values before they’re saved
    • After Triggers: Used to access field values set by the system (Ex: Id, LastModifiedDate). Records that fire after trigger are read-only.

Context Variables

  • Use Context Variables to access the records that caused the trigger to fire. Ex:
    • Trigger.New contains all records that were inserted/updated.
      • Can contain one record or multiple. Can iterate over Trigger.New to get each individual sObject.
    • Trigger.Old provides old version of sObjects before they were updated, or deleted in delete triggers.
    • Triggers can fire when one record is inserted, or when many records are inserted in bulk via the API or Apex.
trigger HelloWorldTrigger on Account (before insert) {
    for(Account a : Trigger.New) {
        a.Description = 'New description';
    }   
}
  • Other context variables return a Boolean to indicate whether the trigger was fired due to an update or other event.
    • All context variables available for triggers:
      • isExecuting, isInsert, isUpdate, isDelete, isBefore, isAfter, isUndelete, new, newMap, old, oldMap, operationType, size
      • Descriptions are here
    • These variables are useful when a trigger combines multiple events. Example:
trigger ContextExampleTrigger on Account (before insert, after insert, after delete) {
    if (Trigger.isInsert) {
        if (Trigger.isBefore) {
            // Process before insert
        } else if (Trigger.isAfter) {
            // Process after insert
        }        
    }
    else if (Trigger.isDelete) {
        // Process after delete
    }
}

Calling a Class Method from a Trigger

  • You can call public utility methods from a trigger, enabling code reuse, reducing trigger size, improving maintenance of Apex code, enabling use of object-oriented programming.
    • Following example trigger shows how to call a static method from a trigger.

Steps

  1. Create the EmailManager class below in a Trailhead org.
  2. Create the ExampleTrigger trigger below on Contact.
  3. Use the code in the third code block in an Execute Anonymous window to create a contact.
public class EmailManager {
    // Public method
	public static void sendMail(String address, String subject, String body) {
    // Create an email message object
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {address};
        mail.setToAddresses(toAddresses);
        mail.setSubject(subject);
        mail.setPlainTextBody(body);
        // Pass this email message to the built-in sendEmail method
        // of the Messaging class
        Messaging.SendEmailResult[] results = Messaging.sendEmail(
                                 new Messaging.SingleEmailMessage[] { mail });

        // Call a helper method to inspect the returned results
        inspectResults(results);
    }

    // Helper method
    private static Boolean inspectResults(Messaging.SendEmailResult[] results) {
        Boolean sendResult = true;

        // sendEmail returns an array of result objects.
        // Iterate through the list to inspect results.
        // In this class, the methods send only one email,
        // so we should have only one result.
        for (Messaging.SendEmailResult res : results) {
            if (res.isSuccess()) {
                System.debug('Email sent successfully');
            }
            else {
                sendResult = false;
                System.debug('The following errors occurred: ' + res.getErrors());                 
            }
        }

        return sendResult;
    }
}
trigger ExampleTrigger on Contact (after insert, after delete) {
    if (Trigger.isInsert) {
        Integer recordCount = Trigger.New.size();
        // Call a utility method from another class
        EmailManager.sendMail('[email protected]', 'Trailhead Trigger Tutorial',
                    recordCount + ' contact(s) were inserted.');
    }
    else if (Trigger.isDelete) {
        // Process after delete
    }
}
Contact c = new Contact(LastName='Test Contact');
insert c;
  • Triggers are often used to access and manage records related to records in the “trigger context.”
    • Trigger below adds a related opportunity for each new or updated account if it doesn’t already have an opportunity associated with it.
trigger AddRelatedRecord on Account(after insert, after update) {
    List<Opportunity> oppList = new List<Opportunity>();

    // Get the related opportunities for the accounts in this trigger
    Map<Id,Account> acctsWithOpps = new Map<Id,Account>(
        [SELECT Id,(SELECT Id FROM Opportunities) FROM Account WHERE Id IN :Trigger.New]);

    // Add an opportunity for each account if it doesn't already have one.
    // Iterate through each account.
    for(Account a : Trigger.New) {
        System.debug('acctsWithOpps.get(a.Id).Opportunities.size()=' + acctsWithOpps.get(a.Id).Opportunities.size());
        // Check if the account already has a related opportunity.
        if (acctsWithOpps.get(a.Id).Opportunities.size() == 0) {
            // If it doesn't, add a default opportunity
            oppList.add(new Opportunity(Name=a.Name + ' Opportunity',
                                       StageName='Prospecting',
                                       CloseDate=System.today().addMonths(1),
                                       AccountId=a.Id));
        }           
    }
    if (oppList.size() > 0) {
        insert oppList;
    }
}
  • Trigger above iterates over all records that are part of the trigger context - the for loop iterates over Trigger.New. It could be more efficient by just iterating over the subset of accounts that don’t have opportunities.

Using Trigger Exceptions

  • Sometimes, its necessary to add restrictions on certain database operations - like preventing records from being saved when certain conditions are met.
    • To prevent saving records in a trigger, call addError() method on the sObject. This throws a fatal error inside a trigger.
  • Example below throws an error if a User tries to delete an Account that has related Opportunities.
trigger AccountDeletion on Account (before delete) {

    // Prevent the deletion of accounts if they have related opportunities.
    for (Account a : [SELECT Id FROM Account
                     WHERE Id IN (SELECT AccountId FROM Opportunity) AND
                     Id IN :Trigger.old]) {
        Trigger.oldMap.get(a.Id).addError(
            'Cannot delete account with related opportunities.');
    }

}

  • To deactivate a trigger, navigate to Setup > search for “Apex Triggers”. Select “Edit” next to the appropriate trigger, then deselect the “Is Active” checkbox.
  • Calling addError() causes the entire set of operations to roll back, except when bulk DML is called with partial success.

Triggers and Callouts

  • Apex allows you to make calls to and integrate Apex code with external Web services.
    • Apex calls to external Web services are referred to as “callouts.”
      • Ex: callout to a stock quote service to get the latest quotes.
  • Callouts from triggers must be done asynchronously so the trigger process doesn’t block you from working while waiting for the external service’s response.
    • Example below uses a hypothetical endpoint URL for illustration purposes only.
    • Asynchronous methods are called future methods and are annotated with @future(callouts=true).
    • Reference Invoking Callouts Using Apex in the Apex Developer Guide for more info.
public class CalloutClass {
    @future(callout=true)
    public static void makeCallout() {
        HttpRequest request = new HttpRequest();
        // Set the endpoint URL.
        String endpoint = 'http://yourHost/yourService';
        request.setEndPoint(endpoint);
        // Set the HTTP verb to GET.
        request.setMethod('GET');
        // Send the HTTP request and get the response.
        HttpResponse response = new HTTP().send(request);
    }
}
trigger CalloutTrigger on Account (before insert, before update) {
    CalloutClass.makeCallout();
}
  • Example “Create an Apex Trigger” class:
    • First, add a Match Billing Address checkbox to Account
trigger AccountAddressTrigger on Account (before insert, before update) {
    for(Account a : Trigger.New) {
        if (a.Match_Billing_Address__c == true) {
            a.ShippingPostalCode = a.BillingPostalCode;
        }
    }
}

Bulk Apex Triggers

Write triggers that operate on collections of sObjects. Write triggers that perform efficient SOQL and DML operations.
  • Apex triggers are optimized to operate in bulk - bulk design patterns are recommended for processing records in triggers.
    • When using bulk design patterns, triggers have better performance, consume less server resources, and are less likely to exceed platform limits.
    • Bulkified code can process large record volumes within governor limits on the Lightning Platform.
    • Following sections demonstrate the main ways of bulkifying Apex code in triggers:
      • Operating on all records in the trigger
      • Performing SOQL and DML on collections of sObjects instead of single sObjects at a time - these apply to any Apex code including SOQL and DML in classes.

Operating on Record Sets

  • Bulkified triggers operate on all sObjects in the trigger context
    • Triggers originating from the UI typically operate on one record
    • Triggers originating from bulk DML or the API operate on a record set
      • Ex: importing many records via the API results in triggers operating on the full record set
  • Programming best practice is to always assumes the trigger operates on a collection of records so it works in all circumstances.
    • Following trigger assumes that only one record caused the trigger to fire - doesn’t work on a full record set when multiple records are inserted in the same transaction.
trigger MyTriggerNotBulk on Account(before insert) {
    Account a = Trigger.New[0];
    a.Description = 'New description';
}
  • Bulkified version iterates over all available sObjects - this loop works if Trigger.New contains either one sObject or many sObjects.
trigger MyTriggerBulk on Account(before insert) {
    for(Account a : Trigger.New) {
        a.Description = 'New description';
    }
}

Performing Bulk SOQL

  • SOQL queries are powerful - they allow you to check a combination of multiple conditions in one query. By using SOQL features, you can write less code and make fewer queries to the database.
    • Making fewer queries helps you avoid hitting query limits: 100 SOQL queries for synchronous Apex or 200 for asynchronous Apex.
  • Avoid making SOQL queries in a loop - example below shows this pattern.
trigger SoqlTriggerNotBulk on Account(after update) {   
    for(Account a : Trigger.New) {
        // Get child records for each account
        // Inefficient SOQL query as it runs once for each account!
        Opportunity[] opps = [SELECT Id,Name,CloseDate
                             FROM Opportunity WHERE AccountId=:a.Id];

        // Do some other processing
    }
}
  • Example below shows a best practice for running SOQL queries. The SOQL query does the heavy lifting and is called once outside the main loop.
    • SOQL uses an inner query SELECT Id FROM Opportunities to get related opportunities of accounts
    • SOQL query is connected to the trigger context records by using the IN clause and binding the Trigger.New variable in the WHERE clause (WHERE Id in :Trigger.New)
      • Filters the accounts to only those records that fired this trigger.
  • After obtaining the records and their related records, the loop iterates over the records of interest using the collection variable, acctsWithOpps
trigger SoqlTriggerBulk on Account(after update) {  
    // Perform SOQL query once.    
    // Get the accounts and their related opportunities.
    List<Account> acctsWithOpps =
        [SELECT Id,(SELECT Id,Name,CloseDate FROM Opportunities)
         FROM Account WHERE Id IN :Trigger.New];

    // Iterate over the returned accounts    
    for(Account a : acctsWithOpps) {
        Opportunity[] relatedOpps = a.Opportunities;  
        // Do some other processing
    }
}
  • If you don’t need the account parent records, you can retrieve only the opportunities that are related to the accounts within this trigger context by specifying a different WHERE clause (WHERE AccountId IN :Trigger.New)
    • Returned opportunities are for all accounts in this trigger context, not for a specific account.
trigger SoqlTriggerBulk on Account(after update) {  
    // Perform SOQL query once.    
    // Get the related opportunities for the accounts in this trigger,
    // and iterate over those records.
    for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.New]) {
        // Do some other processing
    }
}
  • Can reduce the code size by combining the SOQL query with the for loop in one statement: the SOQL for loop.
    • Triggers execute in 200-record batches. So, if 400 records cause a trigger to fire, the trigger fires twice.
      • SOQL for loop is called twice in the example above, but a standalone SOQL query would also be called twice.
trigger SoqlTriggerBulk on Account(after update) {  
    // Perform SOQL query once.    
    // Get the related opportunities for the accounts in this trigger,
    // and iterate over those records.
    for(Opportunity opp : [SELECT Id,Name,CloseDate FROM Opportunity
        WHERE AccountId IN :Trigger.New]) {

        // Do some other processing
    }
}

Performing Bulk DML

  • Perform DML calls on collections of sObjects whenever possible. Performing DML on individual sObjects uses resources inefficiently.
    • Apex runtime allows up to 150 DML calls in one transaction.
  • Example Trigger below is an inefficient example of individually calling DML on single Opportunities:
trigger DmlTriggerNotBulk on Account(after update) {   
    // Get the related opportunities for the accounts in this trigger.        
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.New];          
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {      
        // Update the description when probability is greater
        // than 50% but less than 100%
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            // Update once for each opportunity -- not efficient!
            update opp;
        }
    }    
}
  • Example Trigger below shows how to perform DML in bulk efficiently with only one DML call on a list of opportunities:
    • Note it only uses a single DML call regardless of the number of sObjects.
trigger DmlTriggerBulk on Account(after update) {   
    // Get the related opportunities for the accounts in this trigger.        
    List<Opportunity> relatedOpps = [SELECT Id,Name,Probability FROM Opportunity
        WHERE AccountId IN :Trigger.New];

    List<Opportunity> oppsToUpdate = new List<Opportunity>();
    // Iterate over the related opportunities
    for(Opportunity opp : relatedOpps) {      
        // Update the description when probability is greater
        // than 50% but less than 100%
        if ((opp.Probability >= 50) && (opp.Probability < 100)) {
            opp.Description = 'New description for opportunity.';
            oppsToUpdate.add(opp);
        }
    }

    // Perform DML on a collection
    update oppsToUpdate;
}
  • Example below modifies the trigger example from the previous unit for the AddRelatedRecord trigger.
    • Previous example is not as efficient as it could be - it iterates over all Trigger.New sObject records.
    • Example below modifies the SOQL query to get only records of interest and iterate over those records.
  • Write an SOQL query that returns all accounts in the trigger that don’t have related opportunities:
[SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND
                                             Id NOT IN (SELECT AccountId FROM Opportunity)]
  • Now, iterate over those records using a SOQL for loop:
for(Account a : [SELECT Id,Name FROM Account WHERE Id IN :Trigger.New AND
                                         Id NOT IN (SELECT AccountId FROM Opportunity)]){
}
  • Only missing piece is creation of the default opportunity, which is done in bulk. Here’s the completed, updated trigger:
trigger AddRelatedRecord on Account(after insert, after update) {
    List<Opportunity> oppList = new List<Opportunity>();

    // Add an opportunity for each account if it doesn't already have one.
    // Iterate over accounts that are in this trigger but that don't have opportunities.
    for (Account a : [SELECT Id,Name FROM Account
                     WHERE Id IN :Trigger.New AND
                     Id NOT IN (SELECT AccountId FROM Opportunity)]) {
        // Add a default opportunity for this account
        oppList.add(new Opportunity(Name=a.Name + ' Opportunity',
                                   StageName='Prospecting',
                                   CloseDate=System.today().addMonths(1),
                                   AccountId=a.Id));
    }

    if (oppList.size() > 0) {
        insert oppList;
    }
}

Create a Bulk Apex Trigger

  • Create an bulkified (process 200+ opportunities) Apex Trigger:
    • Object: Opportunity
    • Events: after insert, after update
    • Conditions: Stage = Closed Won
    • Operation: Create a Task
      • Subject = Follow Up Test Task
      • WhatId = the opportunity ID
trigger ClosedOpportunityTrigger on Opportunity (after insert, after update) {
    List<Task> taskList = new List<Task>();

    for (Opportunity o : [SELECT Id FROM Opportunity
                          WHERE Id IN :Trigger.New AND
                          StageName='Closed Won']) {
        taskList.add(new Task(Subject='Follow Up Test Task',
                              WhatId=o.Id));
    }

    if (taskList.size() > 0) {
        insert taskList;
    }
}