Apex Triggers
These are technical notes I compiled while studying using Trailhead, Salesforce's free self-learning portal.
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
- Possible trigger events:
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.
- Can contain one record or multiple. Can iterate over
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:
- All context variables available for triggers:
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
- Create the
EmailManager
class below in a Trailhead org. - Create the
ExampleTrigger
trigger below on Contact. - 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.
- To prevent saving records in a trigger, call
- 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.
- Apex calls to external Web services are referred to as “callouts.”
- 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 theTrigger.New
variable in theWHERE
clause (WHERE Id in :Trigger.New
)- Filters the accounts to only those records that fired this trigger.
- SOQL uses an inner query
- 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.
- Triggers execute in 200-record batches. So, if 400 records cause a trigger to fire, the trigger fires 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;
}
Trigger Example for Getting Related Records
- 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.
- Previous example is not as efficient as it could be - it iterates over all
- 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 TaskWhatId
= 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;
}
}