Trigger.new is the list of records as the platform is about to save them (or has just saved them). Trigger.old is the list of those same records as they existed in the database before the change. The pair lets you detect deltas — “did this field change?” — and react accordingly.
Side-by-side
| Property | Trigger.new | Trigger.old |
|---|---|---|
| Type | List<SObject> | List<SObject> |
| Contents | Post-change records (or to-be-saved) | Pre-change records (from the DB) |
Available in before insert | Yes (writable) | No |
Available in before update | Yes (writable) | Yes (read-only) |
Available in before delete | No | Yes (read-only) |
Available in after insert | Yes (read-only) | No |
Available in after update | Yes (read-only) | Yes (read-only) |
Available in after delete | No | Yes (read-only) |
Available in after undelete | Yes (read-only) | No |
| Mutable | Only in before contexts | Never |
The asymmetry is deliberate: on insert there is no “old” version, on delete there is no “new” version, and on undelete the previously deleted record is the new one being restored.
The pattern: detect a field change
trigger OpportunityTrigger on Opportunity (after update) {
List<Opportunity> wonOpps = new List<Opportunity>();
for (Opportunity opp : Trigger.new) {
Opportunity prior = Trigger.oldMap.get(opp.Id);
if (opp.StageName == 'Closed Won' && prior.StageName != 'Closed Won') {
wonOpps.add(opp);
}
}
if (!wonOpps.isEmpty()) {
WinNotificationService.notify(wonOpps);
}
}
This is the canonical “fire when a field transitions to value X” pattern. Compare same-Id records in Trigger.new and Trigger.oldMap and collect the rows that crossed the threshold.
What you cannot do
- Assign to a field on a
Trigger.oldrecord. It’s read-only — and conceptually, the database state from before the transaction doesn’t exist anymore once the save lands. - Read
Trigger.oldin an insert trigger. There’s nothing to read. The reference is null. - Trust
Trigger.oldto reflect concurrent changes. It captures the row state at the start of the transaction; if another user updated it in parallel, you’ll see your read, not theirs. Use row locking (FOR UPDATE) if that matters.
Common interview follow-ups
- Why is
Trigger.oldavailable on undelete? — It isn’t. Salesforce treats the restored row as a new entrant. OnlyTrigger.newis populated onafter undelete. - What’s the read-only rule for
Trigger.newinaftercontext? — The save has already happened, so the runtime won’t accept in-place edits. To change a field after the fact, doupdateon a freshly constructed list (and guard against recursion). - Is
Trigger.oldcached? — It’s the snapshot the platform took before applying the change. It doesn’t refresh mid-transaction.
Verified against: Apex Developer Guide — Trigger Context Variables. Last reviewed 2026-05-17.