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
Last updated: August 2026

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

EventWhen it firesNotes
before insertBefore new rows are savedNo Id yet on brand-new records
after insertAfter rows are saved (not yet fully committed in the broader transaction model)Ids available
before updateBefore existing rows are savedBoth new and old versions available
after updateAfter existing rows are savedBoth new and old versions available
before deleteBefore rows are removedUse Trigger.old (no Trigger.new)
after deleteAfter rows are removedUse Trigger.old
after undeleteAfter restore from Recycle BinRestored 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.newList<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 separate update DML.
  • Trigger.oldList<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.newMapMap<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.oldMapMap<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

FlagTrue when
Trigger.isBefore / Trigger.isAfterTiming relative to the save of those records
Trigger.isInsertInsert operation
Trigger.isUpdateUpdate operation
Trigger.isDeleteDelete operation
Trigger.isUndeleteUndelete operation
Trigger.isExecutingCode is running in trigger context
Trigger.sizeNumber of records in this invocation (1–200 in a bulk chunk)
Trigger.operationTypeSystem.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, stamp Last_Synced__c)
  • Block saves with addError on 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

  1. One trigger per sObject listing all needed events
  2. Trigger body only routes context → static methods on a handler class
  3. Handler orchestrates; service classes hold domain logic and queries
  4. 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.operationType or 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.

Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Which statement about Trigger.old and Trigger.new is correct?

A
B
C
D
Test Your Knowledge

Why do Salesforce best-practice designs favor a single trigger per object that calls a handler class?

A
B
C
D