Salesforce

Order of Execution in Salesforce: What Runs When

Why This Matters

Most trigger bugs aren't logic bugs. They're sequencing bugs — code that's correct in isolation but behaves unexpectedly because the developer didn't know when in the transaction their code runs. A few specific surprises come up again and again:

  • An after trigger queries a rollup summary field and gets a stale value — the rollup hasn't recalculated yet.
  • A workflow rule's field update causes triggers to fire a second time, doubling the effect of code that wasn't written to run twice.
  • Two triggers on the same object interact in non-deterministic ways because there's no guaranteed ordering between them.
  • A trigger updates a record inside itself, causing a recursive loop that hits governor limits.

Every one of these problems has the same root cause: not knowing the order of execution. Learn it once, and you'll stop being surprised by any of them.

The Complete Sequence

When a record save operation fires — insert, update, upsert, merge, delete, or undelete — Salesforce processes the following steps in order. This applies to both single-record and bulk operations.

  1. Load the original record. For updates, Salesforce fetches the existing record from the database and merges incoming field values on top. For inserts, a new record object is initialised. This is the state available in Trigger.old.
  2. Load and validate the new values. Any field values passed in (from a UI save, API call, or DML statement) are loaded onto the record object.
  3. Execute system validation rules. Required fields, field format validation (date formats, email formats), and field-type constraints are checked before any Apex runs. If these fail, an error is returned immediately and execution stops.
  4. Run all before triggers. All before insert or before update triggers on the object fire. This is the correct place to modify field values — the record is not yet saved to the database, so you can change Trigger.new values directly without any DML. Changes are free (no DML limits consumed).
  5. Re-run system and custom validations. After before triggers have had the opportunity to modify fields, Salesforce validates again. This pass includes: custom validation rules, required field enforcement, unique field constraints, duplicate rules, and foreign-key integrity. If any validation fails here, the transaction rolls back.
  6. Save the record to the database (not yet committed). The record is written to the database in the current transaction. It is not yet committed — the transaction is still open. At this point, the record has an Id (if it's an insert).
  7. Run all after triggers. All after insert or after update triggers fire. The record now exists in the database and has an Id. However, it is not yet committed. To modify the saved record from here, you must perform a separate DML update — that consumes DML limits and will re-enter the order of execution for that update.
  8. Execute assignment rules. For Lead and Case objects only. Assignment rules evaluate and assign owners.
  9. Execute auto-response rules. For Lead and Case objects only. Email auto-responses are queued.
  10. Execute workflow rules. All active workflow rules whose criteria are met are evaluated. Field update actions from workflow rules are collected for application.
  11. If workflow rules produced field updates: re-fire before and after triggers. This is one of the most important steps to understand. If any workflow rule triggered a field update, Salesforce applies those field updates to the record and then fires before triggers and after triggers one additional time. Standard field validations are also re-evaluated. This second pass of triggers does not cause workflow rules to re-evaluate again — that loop is capped at one additional pass.
  12. Execute Processes (Process Builder — legacy). Salesforce Process Builder automations run at this point. In modern orgs, these are being migrated to Flows.
  13. Execute escalation rules. For Case objects only.
  14. Execute entitlement rules. For Case objects only.
  15. Execute rollup summary field recalculations. If the saved record is a detail record in a master-detail relationship, any rollup summary fields on the master record are recalculated. If this changes the master record, the master record's triggers fire (its own full order of execution runs as a nested transaction).
  16. Execute cross-object workflow field updates. If any workflow rules triggered field updates on related (parent) records, those are applied.
  17. Evaluate criteria-based sharing rules. Salesforce re-evaluates sharing rules to determine if the record's visibility has changed as a result of the save.
  18. Commit the transaction to the database. All changes are committed. The record is now permanently written. Any failure before this point rolls back the entire transaction.
  19. Execute post-commit logic. After the database commit, Salesforce processes actions that require the commit to have succeeded: sending workflow email alerts, publishing Platform Events, enqueuing async operations (Queueable Apex, future methods, Batch Apex), and other post-commit integrations.

Critical Implications for Developers

Rollup Summary Fields Are Stale in After Triggers

When your after trigger queries the master record to read a rollup summary field, that rollup has not yet been recalculated for the current transaction (step 15 comes after step 7). You'll get the value from before this save occurred. Never make business decisions in a trigger based on rollup field values from the master — they will be wrong for the current state.

If you need the current aggregate value, compute it yourself in the trigger using the Trigger.new and Trigger.old collections, or query the child records directly.

Workflow Field Updates Re-Fire Triggers

Step 11 means your trigger will run at least twice in any transaction where a workflow rule fires a field update on the same object. If your trigger creates records or sends external callouts, the doubled execution has real consequences. Design your triggers to be idempotent — running them twice should produce the same result as running them once.

Before vs After: Where to Put Your Logic

  • Before triggers are for modifying field values on the record being saved. The record is not yet committed, so you assign to Trigger.new[i].FieldName__c directly. No DML statement needed. No DML limits consumed.
  • After triggers are for operations that depend on the record having an Id (creating related records, sending notifications, updating related objects). To modify the triggering record itself from an after trigger, you must perform an explicit update DML — which costs DML operations and re-enters the order of execution.

Multiple Triggers on the Same Object: Order Is Not Guaranteed

Salesforce makes no guarantee about the execution order of multiple triggers defined on the same object for the same event. If you have AccountTrigger and AccountAuditTrigger both listening to before update, either may run first. The only reliable approach is to consolidate all trigger logic for a given object into a single trigger that delegates to a handler class.

Recursive Triggers

A recursive trigger is one that indirectly causes itself to fire again. The most common scenario:

  1. An after update trigger on Opportunity runs.
  2. Inside the trigger, your code performs a DML update on one of the same Opportunity records.
  3. That DML update re-enters the order of execution for Opportunity, which fires the after trigger again.
  4. The trigger runs again, performs the DML again, fires again — until Salesforce hits the recursion limit and throws a runtime exception.

The same pattern occurs indirectly: trigger A updates Account, Account's trigger updates a related Contact, Contact's trigger updates Account again, re-firing trigger A.

The standard prevention pattern uses a static variable in a helper class. Because static variables persist for the lifetime of a transaction (not across transactions), they serve as a transaction-scoped flag.

The Static Flag Pattern

The simplest version uses a static Boolean to block re-entry:

public class OpportunityTriggerHandler {

    private static Boolean isRunning = false;

    public static void handleAfterUpdate(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        if (isRunning) {
            return;
        }
        isRunning = true;

        try {
            List<Opportunity> toUpdate = new List<Opportunity>();
            for (Opportunity opp : newList) {
                Opportunity old = oldMap.get(opp.Id);
                if (opp.StageName != old.StageName && opp.StageName == 'Closed Won') {
                    toUpdate.add(new Opportunity(
                        Id = opp.Id,
                        Description = 'Stage closed at: ' + System.now().format()
                    ));
                }
            }
            if (!toUpdate.isEmpty()) {
                update toUpdate;
            }
        } finally {
            isRunning = false;
        }
    }
}

The finally block resets the flag even if an exception is thrown, which prevents the flag from permanently blocking subsequent legitimate trigger invocations within the same transaction.

The Set<Id> Pattern for Finer Control

The Boolean flag blocks all re-entry. In bulk operations, this can cause problems: if a batch of 200 records triggers an update that includes some new records not in the original set, the Boolean flag blocks those too. A Set<Id> of already-processed record IDs gives you per-record control:

public class OpportunityTriggerHandler {

    private static Set<Id> processedIds = new Set<Id>();

    public static void handleAfterUpdate(List<Opportunity> newList, Map<Id, Opportunity> oldMap) {
        List<Opportunity> unprocessed = new List<Opportunity>();

        for (Opportunity opp : newList) {
            if (!processedIds.contains(opp.Id)) {
                unprocessed.add(opp);
            }
        }

        if (unprocessed.isEmpty()) {
            return;
        }

        // Mark as processed before DML to block re-entry
        for (Opportunity opp : unprocessed) {
            processedIds.add(opp.Id);
        }

        List<Opportunity> toUpdate = new List<Opportunity>();
        for (Opportunity opp : unprocessed) {
            Opportunity old = oldMap.get(opp.Id);
            if (opp.StageName != old.StageName && opp.StageName == 'Closed Won') {
                toUpdate.add(new Opportunity(
                    Id = opp.Id,
                    Description = 'Stage closed at: ' + System.now().format()
                ));
            }
        }

        if (!toUpdate.isEmpty()) {
            update toUpdate;
        }
    }
}

The Trigger and Handler Together

The trigger itself should contain no logic — just routing. All logic lives in the handler class:

trigger OpportunityTrigger on Opportunity (
    before insert, before update,
    after insert, after update, after delete
) {
    if (Trigger.isAfter && Trigger.isUpdate) {
        OpportunityTriggerHandler.handleAfterUpdate(Trigger.new, Trigger.oldMap);
    }
    if (Trigger.isBefore && Trigger.isUpdate) {
        OpportunityTriggerHandler.handleBeforeUpdate(Trigger.new, Trigger.oldMap);
    }
    if (Trigger.isBefore && Trigger.isInsert) {
        OpportunityTriggerHandler.handleBeforeInsert(Trigger.new);
    }
}

Testing Recursion Prevention

Write an explicit test that verifies the handler doesn't run twice for a record it has already processed:

@IsTest
private class OpportunityTriggerHandlerTest {

    @TestSetup
    static void setup() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        Opportunity opp = new Opportunity(
            Name        = 'Test Opp',
            AccountId   = acc.Id,
            StageName   = 'Prospecting',
            CloseDate   = Date.today().addDays(30)
        );
        insert opp;
    }

    @IsTest
    static void whenStageClosedWon_descriptionIsSet_andNotDoubled() {
        Opportunity opp = [SELECT Id, StageName FROM Opportunity LIMIT 1];

        Test.startTest();
        opp.StageName = 'Closed Won';
        update opp;
        Test.stopTest();

        Opportunity result = [SELECT Id, Description FROM Opportunity WHERE Id = :opp.Id];

        // Description should be set exactly once, not duplicated
        System.assertNotEquals(null, result.Description,
            'Description should be set when stage moves to Closed Won');

        // If the handler ran twice, the description would be set twice (appended or overwritten)
        // Verify it doesn't contain duplicated timestamp markers
        System.assert(
            result.Description.countMatches('Stage closed at:') == 1,
            'Handler ran more than once: ' + result.Description
        );
    }

    @IsTest
    static void staticSetIsTransactionScoped_resetsBetweenTests() {
        // Static variables reset between test method invocations in separate transactions.
        // This test confirms a fresh transaction sees an empty processedIds set.
        Opportunity opp = [SELECT Id, StageName FROM Opportunity LIMIT 1];

        Test.startTest();
        opp.StageName = 'Closed Won';
        update opp;
        Test.stopTest();

        Opportunity result = [SELECT Id, Description FROM Opportunity WHERE Id = :opp.Id];
        System.assertNotEquals(null, result.Description);
    }
}

Quick Reference: The Sequence on One Page

Step What Runs Notes
1Load original recordTrigger.old is populated
2Merge new valuesTrigger.new is populated
3System validationsRequired fields, type checks — fail fast
4Before triggersModify Trigger.new freely; no DML needed
5Custom validations & duplicate rulesPost-trigger field values are validated
6Save to database (not committed)Record gets an Id on insert
7After triggersId available; DML to change triggering record costs limits
8–9Assignment & auto-response rulesLeads and Cases only
10Workflow rulesField updates collected
11Re-fire before + after triggersOnly if workflow field updates occurred
12Process BuilderLegacy; being replaced by Flows
13–14Escalation & entitlement rulesCases only
15Rollup recalculationsMay fire master record's triggers
16Cross-object workflow updates
17Sharing rules evaluation
18Database commitAll-or-nothing; failure here rolls back everything
19Post-commit logicEmails, Platform Events, async Apex enqueued

The order of execution is the foundation of reliable Salesforce development. Know that before triggers let you modify fields for free while after triggers require a DML update to do the same. Know that rollup summary fields are calculated after after triggers run, so never rely on them in the same transaction. Know that workflow field updates cause a second pass of triggers — write every trigger as if it might run twice. For recursion, use a static Set<Id> in a handler class to gate which records your trigger processes, and consolidate all logic for one object into one trigger to keep execution predictable.

← All articles

Frequently Asked Questions

Why does an after trigger return a stale value when querying a rollup summary field?

Rollup summary fields on parent records are recalculated after the transaction commits, not during it. When an after trigger queries a parent's rollup field, the child record has been saved to the database but the transaction is still open, so the parent's aggregate value hasn't updated yet. To work around this, use asynchronous Apex (a future method or queueable) to query the rollup after the transaction closes.

When a workflow field update causes triggers to fire a second time, which steps in the order of execution are repeated?

After workflow rules and field updates execute, Salesforce re-runs the entire trigger sequence from the beginning — before triggers, validations, the database save, and after triggers all fire again on the same record. This means any trigger that isn't written with a static flag or recursion guard will execute its logic twice, which commonly causes doubled DML operations or incorrect field calculations.

Is there a guaranteed execution order when multiple triggers exist on the same object?

No — Salesforce does not guarantee the order in which multiple triggers on the same object fire. The execution order is non-deterministic and can change between deployments or org refreshes. The standard solution is to maintain a single trigger per object that delegates to an Apex handler class, giving full control over the sequence of logic within that one trigger.

At what point in the order of execution does a newly inserted record first have an Id available?

The record receives its Id when it is saved to the database, which occurs after all before triggers and the second round of validations have completed. This means before triggers cannot reference Trigger.new[i].Id on an insert — it will be null. After triggers are the earliest point where the Id is accessible and can safely be used in queries or related record operations.