The Apex trigger syntax is short and rigid. The shape never changes — only the object, the events, and the body do.
The grammar
trigger TriggerName on ObjectAPIName (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
// body
}
Three parts:
trigger TriggerName— a unique name in the org. Convention:<Object>Trigger, e.g.AccountTrigger.on ObjectAPIName— the SObject this trigger fires on. One trigger fires on exactly one object.(events)— one or more of seven combinations:before insert,before update,before delete,after insert,after update,after delete,after undelete. (before undeletedoesn’t exist — there’s nothing to validate before a restore.)
A minimal example
A before-insert trigger that defaults the Rating__c field to 'Warm' if the user didn’t supply one.
trigger AccountDefaultRating on Account (before insert) {
for (Account a : Trigger.new) {
if (a.Rating == null) {
a.Rating = 'Warm';
}
}
}
A few things to notice:
- We don’t
updatethe records — we mutate them. In abeforetrigger,Trigger.newis the in-memory copy that’s about to be saved. Mutate it; the save uses your changes. for (Account a : Trigger.new)works becauseTrigger.newis strongly typed to the object the trigger is declared on.- No SOQL, no DML — the whole thing runs on what’s already in memory.
A more realistic example — Account → Contact roll-up
When a contact is inserted or deleted, recompute Total_Contacts__c on the parent account.
trigger ContactCountOnAccount on Contact (after insert, after delete) {
Set<Id> accountIds = new Set<Id>();
if (Trigger.isInsert) {
for (Contact c : Trigger.new) {
if (c.AccountId != null) accountIds.add(c.AccountId);
}
} else { // delete
for (Contact c : Trigger.old) {
if (c.AccountId != null) accountIds.add(c.AccountId);
}
}
if (accountIds.isEmpty()) return;
// One aggregate query covers all parents
Map<Id, Integer> countByAccount = new Map<Id, Integer>();
for (AggregateResult ar : [
SELECT AccountId acc, COUNT(Id) cnt
FROM Contact
WHERE AccountId IN :accountIds
GROUP BY AccountId
]) {
countByAccount.put((Id) ar.get('acc'), (Integer) ar.get('cnt'));
}
// Build the parent updates
List<Account> toUpdate = new List<Account>();
for (Id aId : accountIds) {
toUpdate.add(new Account(
Id = aId,
Total_Contacts__c = countByAccount.containsKey(aId) ? countByAccount.get(aId) : 0
));
}
update toUpdate;
}
This single trigger shows the patterns interviewers look for:
- Bulk-safe — Set/Map, no SOQL/DML in a loop.
- Context-aware — checks
Trigger.isInsertto decide whether to readTrigger.neworTrigger.old. - Aggregate query — pushes counting to the database instead of looping in Apex.
What lives where: Trigger.new vs Trigger.old
| Variable | Type | Available in… |
|---|---|---|
Trigger.new | List<SObject> | All insert/update events; null in delete |
Trigger.newMap | Map<Id, SObject> | After-insert (Ids exist), all update events |
Trigger.old | List<SObject> | Update and delete events |
Trigger.oldMap | Map<Id, SObject> | Update and delete events |
Production shape — delegate to a handler
In real projects, the trigger file itself contains exactly one line per event. All logic lives in a handler class for testability and the one trigger per object rule.
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
new AccountTriggerHandler().run();
}
What interviewers are really looking for
A junior writes the syntax and a one-line example. A mid-level developer writes a bulk-safe rollup with Set<Id>, no SOQL in loops, and the Trigger.isInsert branch. A senior delegates to a handler class with a recursion guard and a bypass switch. Show the third tier — even briefly — and you’ve moved out of the “remembers Apex” bucket into the “writes Apex for production” bucket.
Verified against: Apex Developer Guide — Triggers. Last reviewed 2026-05-17.