Salesforce Apex Triggers: Automate Actions on Records
Flow Builder handles most record-based automation. But when the logic is complex, involves large data volumes, or needs precise control over timing, developers write Apex Triggers. A trigger is a block of Apex code that Salesforce executes automatically whenever a specific database event occurs on a particular object — no user click required.
What Is a Trigger?
A trigger is a piece of code that says: "When this event happens on this object, run this logic." Events include inserting a new record, updating an existing one, deleting it, or undeleting it. Triggers fire at the database level, which makes them the most reliable and powerful form of automation in Salesforce.
The Motion Sensor Analogy
MOTION SENSOR LIGHT:
Trigger: Someone walks into the room
Action: Light turns on automatically
APEX TRIGGER:
Trigger: A new Opportunity is inserted with Stage = "Closed Won"
Action: Create an onboarding Case, email the delivery team,
update the Account's customer tier — all automatically
Trigger Syntax
Every trigger follows this structure:
trigger TriggerName on ObjectName (trigger_events) {
// Your logic here
}
A Simple Trigger Example
trigger OpportunityTrigger on Opportunity (after update) {
List<Case> casesToCreate = new List<Case>();
for (Opportunity opp : Trigger.new) {
Opportunity oldOpp = Trigger.oldMap.get(opp.Id);
// Check if Stage just changed to Closed Won
if (opp.StageName == 'Closed Won'
&& oldOpp.StageName != 'Closed Won') {
Case newCase = new Case();
newCase.Subject = 'Onboarding: ' + opp.Name;
newCase.AccountId = opp.AccountId;
newCase.Priority = 'High';
newCase.Status = 'New';
casesToCreate.add(newCase);
}
}
if (!casesToCreate.isEmpty()) {
insert casesToCreate;
}
}
Trigger Events
A trigger specifies which database events cause it to fire. Events are listed in the trigger declaration, separated by commas:
| Event | When It Fires |
|---|---|
| before insert | Before a new record is saved to the database |
| after insert | After a new record is saved to the database |
| before update | Before an existing record's changes are saved |
| after update | After an existing record's changes are saved |
| before delete | Before a record is deleted |
| after delete | After a record is deleted |
| after undelete | After a record is restored from the Recycle Bin |
Before vs. After Triggers
- Before triggers — run before the record is written to the database. Use them to validate or modify field values on the record being saved. Changes to Trigger.new take effect without a DML statement — efficient and avoids extra operations.
- After triggers — run after the record is committed. Use them when you need the record's final Id (assigned by the database) or when you need to update related records on other objects.
Trigger Context Variables
Inside every trigger, Salesforce provides special variables that give you access to the records being processed:
| Context Variable | What It Contains | Available In |
|---|---|---|
| Trigger.new | List of new record versions (after changes) | insert, update, undelete |
| Trigger.old | List of original record versions (before changes) | update, delete |
| Trigger.newMap | Map of Id to new record version | insert, update, undelete |
| Trigger.oldMap | Map of Id to old record version | update, delete |
| Trigger.isInsert | TRUE if this is an insert event | All events |
| Trigger.isUpdate | TRUE if this is an update event | All events |
| Trigger.isDelete | TRUE if this is a delete event | All events |
| Trigger.isBefore | TRUE if running before the save | All events |
| Trigger.isAfter | TRUE if running after the save | All events |
The Handler Class Pattern
A common mistake beginners make is putting all logic directly inside the trigger file. As the logic grows, the trigger becomes impossible to maintain or test. Professional developers use the Handler Class Pattern: the trigger file stays thin and delegates all logic to a separate Apex class.
// TRIGGER FILE (thin — just routes to the handler)
trigger OpportunityTrigger on Opportunity (before insert, after update) {
OpportunityTriggerHandler handler = new OpportunityTriggerHandler();
if (Trigger.isBefore && Trigger.isInsert) {
handler.beforeInsert(Trigger.new);
}
if (Trigger.isAfter && Trigger.isUpdate) {
handler.afterUpdate(Trigger.new, Trigger.oldMap);
}
}
// HANDLER CLASS (contains all the logic)
public class OpportunityTriggerHandler {
public void beforeInsert(List<Opportunity> newOpps) {
for (Opportunity opp : newOpps) {
if (opp.LeadSource == null) {
opp.LeadSource = 'Web';
}
}
}
public void afterUpdate(List<Opportunity> newOpps,
Map<Id, Opportunity> oldMap) {
// complex logic here
}
}
Benefits of this pattern:
- Easy to unit test — test the handler class directly without simulating trigger events
- Easy to read — each method has a clear, named purpose
- Easy to maintain — adding logic means adding a method, not untangling a long trigger file
One Trigger Per Object Rule
Salesforce allows multiple triggers on the same object, but the order in which they fire is not guaranteed. Two triggers on Opportunity might run in any sequence — making behavior unpredictable and debugging very difficult. The professional standard is one trigger per object that routes to a handler class. All logic for that object lives in one organized handler.
Trigger Best Practices
- Keep triggers thin — put all logic in a handler class.
- Bulkify all logic — assume Trigger.new always contains up to 200 records, never just one.
- Never write SOQL or DML inside a loop.
- Use Trigger.oldMap to detect field changes accurately.
- Write test classes that cover at least 75% of your trigger code — Salesforce requires this before deployment to Production.
Key Points
- Triggers fire automatically on database events: before/after insert, update, delete, and undelete.
- Before triggers modify records without extra DML; after triggers handle related record updates and require DML.
- Context variables like Trigger.new, Trigger.old, Trigger.newMap, and Trigger.oldMap give access to record data inside the trigger.
- Use the Handler Class Pattern — keep the trigger file thin and put logic in a separate Apex class.
- Write one trigger per object and always bulkify logic to handle up to 200 records at a time.
