SfSkills Domains Docs GitHub

Home / apex / apex-trigger-context-variables

apex-trigger-context-variables

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 / handler architecture and one-trigger-per-object design — use apex/trigger-framework. NOT for bulk patterns and Map<Id, SObject> collection handling — use apex/apex-collections-patterns.

Open the skill folder on GitHub


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

Core Concepts

The event matrix

EventnewnewMapoldoldMap
before insert✓null (no Id)nullnull
after insert✓✓nullnull
before update✓✓✓✓
after update✓✓✓✓
before deletenullnull✓✓
after deletenullnull✓✓
after undelete✓✓nullnull

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();
}
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

NeedAccess
Read new values, no Id neededTrigger.new
Need Id (after insert and beyond)Trigger.newMap
Compare old vs newTrigger.new + Trigger.oldMap.get(record.Id)
Delete validationTrigger.old / Trigger.oldMap
Determine context programmaticallyTrigger.isInsert, Trigger.isBefore, etc.
  1. Decide trigger events (before/after × DML).
  2. Consult the event matrix; confirm which context variables are populated.
  3. Route each event to a dedicated handler method — never monolithic trigger body.
  4. For before-update, pair Trigger.new iteration with Trigger.oldMap.get(rec.Id) lookup.
  5. Add recursion guard via framework static flag.
  6. Bulk-safe everything: no SOQL/DML in loops.
  7. Unit test each event with Test.startTest() and explicit record setup.

Review Checklist

Salesforce-Specific Gotchas

  1. Trigger.newMap is null in before-insert. Records have no IDs yet; you cannot index by Id until after-insert.
  2. Modifying Trigger.new in after events throws. Use a separate DML pass.
  3. Trigger.old is NOT populated in insert contexts. Default to null checks for pre-existing state.

Output Artifacts

ArtifactDescription
Event matrix referenceQuick-lookup table for which variable in which event
Handler templateSkeleton trigger + handler class
Recursion guard classFramework static boolean

Package files