name: apex-trigger-context-variables description: "Apex Trigger.new / Trigger.old / Trigger.newMap / Trigger.oldMap / Trigger.isInsert etc.: when each is populated, null-safety, recursion depth, trigger event matrix. NOT for trigger framework architecture (use apex-trigger-handler-framework). NOT for bulk patterns (use apex-bulkification-patterns)." category: apex salesforce-version: "Spring '25+" well-architected-pillars:
- Reliability tags:
- apex
- triggers
- context-variables
- trigger-new
- trigger-old triggers:
- "why is trigger.oldmap null in before insert"
- "trigger.new vs trigger.newmap performance difference"
- "trigger context variable matrix insert update delete"
- "trigger.isexecuting vs system.isbatch distinguish context"
- "trigger.newmap nullpointerexception before insert"
- "accessing old values in after update trigger" inputs:
- Trigger events in scope (before/after × insert/update/delete/undelete)
- Whether Trigger.newMap/oldMap needed
- Recursion-control requirement outputs:
- Correct context-variable usage per event
- Null-safe access pattern
- Event matrix documentation dependencies: [] version: 1.0.0 author: Pranav Nagrecha updated: 2026-04-21
Apex Trigger Context Variables
Activate when writing or reviewing an Apex trigger or a trigger handler method that consumes Trigger context variables. Each variable (Trigger.new, Trigger.old, Trigger.newMap, Trigger.oldMap, Trigger.isExecuting, the event booleans) is populated only in specific events; using one in the wrong event causes NullPointerException or wrong semantics.
Before Starting
- Memorize the context matrix.
Trigger.oldandTrigger.oldMapare null inbefore insertandafter insert.Trigger.newMapis null inbefore insert(records have no Id yet). - Know the delete semantics. In
before delete/after delete, onlyTrigger.oldandTrigger.oldMapare populated.Trigger.newis null. - Guard recursion. Triggers can re-enter; a static
Set<Id> processedIdsor a framework flag is mandatory for trigger-causing-trigger scenarios.
Core Concepts
The event matrix
| Event | new | newMap | old | oldMap |
|---|---|---|---|---|
| before insert | ✓ | null (no Id) | null | null |
| after insert | ✓ | ✓ | null | null |
| before update | ✓ | ✓ | ✓ | ✓ |
| after update | ✓ | ✓ | ✓ | ✓ |
| before delete | null | null | ✓ | ✓ |
| after delete | null | null | ✓ | ✓ |
| after undelete | ✓ | ✓ | null | null |
Modifying vs reading
Trigger.new records are read-write in before events (mutate fields directly). In after events, Trigger.new is read-only — attempts to modify throw. Updates in after require explicit DML on a separate list.
Recursion and re-entry
Updating a record inside its own trigger re-fires the trigger. Guard with a framework-level static boolean or an ID set.
Trigger.size
Scalar count — useful for bulk heuristics, but prefer Trigger.new.size().
Common Patterns
Pattern: Before-update field stamping
for (Account a : Trigger.new) {
Account oldA = Trigger.oldMap.get(a.Id);
if (a.Industry != oldA.Industry) a.Industry_Changed_On__c = Datetime.now();
}
Pattern: After-insert related-record creation
List<Contact> defaults = new List<Contact>();
for (Account a : Trigger.new) defaults.add(new Contact(AccountId = a.Id, LastName = 'Primary'));
insert defaults;
Pattern: Before-delete gate
for (Account a : Trigger.old) {
if (a.Is_Protected__c) a.addError('Cannot delete protected account');
}
Decision Guidance
| Need | Access |
|---|---|
| Read new values, no Id needed | Trigger.new |
| Need Id (after insert and beyond) | Trigger.newMap |
| Compare old vs new | Trigger.new + Trigger.oldMap.get(record.Id) |
| Delete validation | Trigger.old / Trigger.oldMap |
| Determine context programmatically | Trigger.isInsert, Trigger.isBefore, etc. |
Recommended Workflow
- Decide trigger events (before/after × DML).
- Consult the event matrix; confirm which context variables are populated.
- Route each event to a dedicated handler method — never monolithic trigger body.
- For before-update, pair
Trigger.newiteration withTrigger.oldMap.get(rec.Id)lookup. - Add recursion guard via framework static flag.
- Bulk-safe everything: no SOQL/DML in loops.
- Unit test each event with
Test.startTest()and explicit record setup.
Review Checklist
- Correct context variable per event (not relying on null-populated ones)
- Null-safe access for oldMap in before-insert / after-insert
- Handler methods split by event
- Recursion guard in place
- Bulkified: no per-record SOQL/DML
- Before events mutate
Trigger.new; after events use DML on new lists - Tests cover each event with bulk collections (200+)
Salesforce-Specific Gotchas
Trigger.newMapis null in before-insert. Records have no IDs yet; you cannot index by Id until after-insert.- Modifying
Trigger.newin after events throws. Use a separate DML pass. Trigger.oldis NOT populated in insert contexts. Default to null checks for pre-existing state.
Output Artifacts
| Artifact | Description |
|---|---|
| Event matrix reference | Quick-lookup table for which variable in which event |
| Handler template | Skeleton trigger + handler class |
| Recursion guard class | Framework static boolean |
Related Skills
apex/apex-trigger-handler-framework— framework architectureapex/apex-bulkification-patterns— bulk-safe DMLapex/apex-async-patterns— async DML patterns when sync unsafe