Short Answer
On an upsert, each record fires the trigger once in the context that matches the database action performed: either an insert or an update. For that record you will get the before and after contexts for the actual operation (so before insert + after insert for inserts; before update + after update for updates). A mixed upsert (some records inserted, some updated) processes each record according to whether it was inserted or updated.
Detailed Explanation (Salesforce Apex Triggers & Upsert)
“Upsert” is a combined operation — Salesforce decides for each incoming record whether to insert (new) or update (existing). Triggers execute per record based on the action that happens:
- If a record is inserted by upsert:
before insertandafter insertfire for that record. - If a record is updated by upsert:
before updateandafter updatefire for that record.
So a trigger does not run both insert and update contexts for the same record during a single upsert — it runs only the appropriate contexts (insert or update) for that record.
Important Notes & Edge Cases
- Bulk upsert: When you upsert a collection of records in one API call, Salesforce batches them and runs triggers in bulk. Each record in the collection will be handled as insert or update individually, but triggers are invoked in bulk context (Trigger.new will contain all records of the same trigger context).
- Before/After contexts: For the action that actually occurs you will always get both before and after contexts (unless you wrote a trigger only for before or after). For example, an inserted record will generate both
before insertandafter insertevents. - Workflow, Process Builder, Flow, or other automations that change records can cause additional trigger executions within the same transaction (for example, field updates that cause an update after insert), so a record may indirectly cause triggers to run again.
- When using external IDs for upsert, Salesforce matches on that external ID to determine update vs insert. Your trigger logic should rely on
Trigger.isInsert/Trigger.isUpdaterather than only checking the presence ofId, to be safe in mixed scenarios.
Example: Typical Trigger Structure to Handle Upsert
Use the standard Trigger context checks to separate logic for insert vs update:
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
if (Trigger.isBefore) {
if (Trigger.isInsert) {
// handle before insert logic
}
if (Trigger.isUpdate) {
// handle before update logic
}
}
if (Trigger.isAfter) {
if (Trigger.isInsert) {
// handle after insert logic
}
if (Trigger.isUpdate) {
// handle after update logic
}
}
}
Summary
For each record in an upsert call, the trigger executes only in the context of the operation performed (insert or update). You get the respective before and after events for that operation. However, automations or additional DML in the same transaction can cause the trigger to run again.








Leave a Reply