8.1 Trigger Structure & Context Variables
Key Takeaways
- Apex triggers fire on sObject DML events: before/after insert, update, delete, and undelete (before undelete is not supported)
- Trigger.new holds new versions of records; Trigger.old holds prior versions on update/delete; Map variants index by Id for bulk-safe lookups
- Context booleans (Trigger.isInsert, isUpdate, isDelete, isUndelete, isBefore, isAfter) route logic without guessing the event
- Use before triggers to validate and modify fields on the triggering records without extra DML; use after triggers for related-record DML and work that needs permanent Ids
- Platform best practice is one trigger per object that delegates to a handler class—never scatter business rules across multiple unmanaged triggers
8.1 Trigger Structure & Context Variables
Quick Answer: An Apex trigger runs automatically when records of a chosen sObject are inserted, updated, deleted, or undeleted. Use before context to validate and change fields on the triggering records without extra DML; use after context for related DML and anything that needs committed Ids. Master Trigger.new, Trigger.old, their Map forms, and the isInsert / isUpdate / isBefore flags so you route bulk-safe logic correctly on Platform Developer I.
Triggers are the classic Apex entry point for record automation. Even when Flow is preferred for simple declarative work, the exam expects you to read trigger syntax, choose before vs after correctly, and use context variables under bulk data loads—not single-record assumptions.
Trigger Declaration Syntax
A trigger is bound to one sObject and one or more events:
trigger AccountTrigger on Account (
before insert,
before update,
after insert,
after update,
before delete,
after delete,
after undelete
) {
// route by context, then call a handler (see 8.2)
}
Supported events
| Event | When it fires | Notes |
|---|---|---|
| before insert | Before new rows are saved | No Id yet on brand-new records |
| after insert | After rows are saved (not yet fully committed in the broader transaction model) | Ids available |
| before update | Before existing rows are saved | Both new and old versions available |
| after update | After existing rows are saved | Both new and old versions available |
| before delete | Before rows are removed | Use Trigger.old (no Trigger.new) |
| after delete | After rows are removed | Use Trigger.old |
| after undelete | After restore from Recycle Bin | Restored records in Trigger.new |
There is no before undelete. Merging, cascade deletes, and undelete have nuanced context availability—read the event table carefully on scenario questions.
Trigger names must be unique in the org and typically end with Trigger (for example ContactTrigger). The body is not a class: you cannot declare methods inside a trigger. Put reusable logic in Apex classes and call them from the trigger.
Context Variables You Must Know
Salesforce populates static context on the Trigger class for the duration of the invocation:
Trigger.new and Trigger.old
- Trigger.new —
List<sObject>of the new versions of the records. Present on insert, update, and undelete. On before insert/update, you may change field values on these sObjects and those changes participate in the save without a separateupdateDML. - Trigger.old —
List<sObject>of the prior versions. Present on update and delete. Read-only. Compare old vs new to detect field changes.
for (Account a : Trigger.new) {
Account prior = Trigger.oldMap.get(a.Id); // update only
if (prior != null && a.Name != prior.Name) {
// name changed
}
}
On delete, iterate Trigger.old (or oldMap). Trigger.new is not available in delete context.
Trigger.newMap and Trigger.oldMap
- Trigger.newMap —
Map<Id, sObject>of new records by Id. Available when records already have Ids (update, after insert, undelete paths as documented). Not available in before insert in the usual sense for brand-new rows without Ids. - Trigger.oldMap —
Map<Id, sObject>of prior versions by Id on update/delete.
Maps are the bulk-safe way to fetch the counterpart of a record without scanning lists repeatedly:
for (Contact c : Trigger.new) {
Contact oldC = Trigger.oldMap.get(c.Id);
if (oldC.Email != c.Email) {
// email changed—collect for later work
}
}
Operation and timing flags
| Flag | True when |
|---|---|
Trigger.isBefore / Trigger.isAfter | Timing relative to the save of those records |
Trigger.isInsert | Insert operation |
Trigger.isUpdate | Update operation |
Trigger.isDelete | Delete operation |
Trigger.isUndelete | Undelete operation |
Trigger.isExecuting | Code is running in trigger context |
Trigger.size | Number of records in this invocation (1–200 in a bulk chunk) |
Trigger.operationType | System.TriggerOperation enum (INSERT, UPDATE, DELETE, UNDELETE with BEFORE/AFTER) |
Modern handlers often switch on Trigger.operationType instead of stacking many booleans:
switch on Trigger.operationType {
when BEFORE_INSERT {
AccountTriggerHandler.beforeInsert(Trigger.new);
}
when BEFORE_UPDATE {
AccountTriggerHandler.beforeUpdate(Trigger.new, Trigger.oldMap);
}
when AFTER_INSERT {
AccountTriggerHandler.afterInsert(Trigger.new);
}
when AFTER_UPDATE {
AccountTriggerHandler.afterUpdate(Trigger.new, Trigger.oldMap);
}
when BEFORE_DELETE {
AccountTriggerHandler.beforeDelete(Trigger.old);
}
when AFTER_DELETE {
AccountTriggerHandler.afterDelete(Trigger.old);
}
when AFTER_UNDELETE {
AccountTriggerHandler.afterUndelete(Trigger.new);
}
}
Before vs After: Choosing the Right Context
Before triggers — validate and stamp the same records
Use before when you need to:
- Default or normalize field values (
Status__c = 'New', trim strings, stampLast_Synced__c) - Block saves with
addErroron a record or field - Enforce cross-field rules that should fail the DML before it persists
- Avoid an extra DML on the same records (changing Trigger.new in before update/insert is free relative to after-update that updates the same rows again)
// before insert or before update
for (Opportunity o : Trigger.new) {
if (o.Amount != null && o.Amount < 0) {
o.Amount.addError('Amount cannot be negative');
}
if (o.CloseDate == null) {
o.CloseDate = Date.today().addDays(30);
}
}
addError on a record or field marks that row (or the whole operation depending on allOrNone behavior) as failed. Prefer field-level messages for UI clarity.
Before insert caveat: records do not yet have Salesforce Ids. You cannot put new records into a Map by Id, create children that need ParentId, or query “this” record by Id. Related DML that needs the new Id belongs in after insert.
After triggers — related work and Id-dependent logic
Use after when you need to:
- Insert/update/delete related records (Tasks, child custom objects, junction rows)
- Callout-prep or async work that should only run after a successful save path for these records
- Read values that platform logic may have set during the save
- Work with Ids of newly inserted parents
// after insert
List<Task> tasks = new List<Task>();
for (Case c : Trigger.new) {
tasks.add(new Task(
WhatId = c.Id,
Subject = 'Follow up new Case',
Status = 'Not Started',
Priority = 'Normal'
));
}
if (!tasks.isEmpty()) {
insert tasks;
}
Do not modify Trigger.new field values in after context expecting them to persist on the same save—those records are already past the before-save modification window. Changing fields in after usually requires a separate update, which re-enters the order of execution and can cause recursion (covered in 8.3 and 8.4).
One Trigger per Object (Best Practice Overview)
Multiple triggers on the same object and event are allowed, but execution order among peer triggers is not guaranteed. That creates race conditions, double-processing risk, and untestable ordering assumptions.
Recommended shape
- One trigger per sObject listing all needed events
- Trigger body only routes context → static methods on a handler class
- Handler orchestrates; service classes hold domain logic and queries
- Optional framework (trigger handler base class) for recursion flags and bypass
This pattern is the default “best practice” answer on exam design questions. Fat triggers full of SOQL, nested loops, and duplicated rules are anti-patterns even if they “work” for one record in the Developer Console.
Bulk Context Is Always On
Triggers always receive lists, even when a user saves one record. Platform bulk APIs can deliver up to 200 records per chunk. Code that assumes Trigger.new[0] only, or runs SOQL/DML once per iteration without collecting Ids, fails governors under load. Section 8.2 deepens bulk patterns; for this section, treat every loop over Trigger.new/old as a bulk set.
Exam Checklist for Context Variables
- Insert before: new yes, old no, Ids not yet assigned
- Update: new and old (and maps) yes—compare for change detection
- Delete: old yes, new no
- Undelete: after only, new yes
- Field edits without extra DML → before
- Related DML needing parent Id → after insert
- Prefer
Trigger.operationTypeor clear isBefore/isInsert routing over guessing - One trigger per object + handler class for maintainability questions
Master structure and context first; bulk frameworks, order of execution, and recursion build directly on these rules.
A developer must set a default Priority on Case during creation and reject negative custom Score__c values before they are stored. Which trigger context is most appropriate?
Which statement about Trigger.old and Trigger.new is correct?
Why do Salesforce best-practice designs favor a single trigger per object that calls a handler class?