← Back
Platform2026-03-29

Salesforce Order of Execution Deep Dive: The 20-Step Chain, Trigger Re-Firing, and Recursion Defense

A Mysterious Duplicate Record That Reveals Everything

Here's the scenario: an after trigger on Opportunity creates an Order record whenever the stage changes to Closed Won. Day one in production, every deal generates two Orders. After hours of debugging, the culprit turns out to be a Workflow Rule Field Update on an unrelated field — it re-fires the trigger, and Trigger.old still holds the original values from before the initial update, so the stage-change condition matches again.

Bugs like this always trace back to one thing: not understanding Salesforce's Order of Execution. It governs exactly what happens — and in what sequence — between clicking Save and the record actually being committed to the database. Without a solid grasp of this chain, triggers and Flows stepping on each other, Validation Rules that "sometimes don't fire," and field values getting mysteriously overwritten are just a matter of time.

The Complete 20-Step Execution Order

Below is the official execution order based on API v60+, applicable to insert, update, and upsert operations. Delete and undelete have their own simplified versions — here we focus on the most common save scenario.

StepWhat ExecutesWhat Developers Should Watch
1Load original record from database (or initialize defaults for insert)Trigger.old values are set at this point
2Overwrite fields with new values from the request; run system-level validation (field types, length, required fields)UI and API submissions have different validation behavior
3Execute Before-Save Record-Triggered FlowsRuns before before triggers; can modify the current record without DML
4Execute all Before TriggersExecution order of multiple triggers on the same object is non-deterministic
5Run system validation again + custom Validation RulesValidation Rules run after before triggers
6Execute Duplicate RulesMay block save or log the duplicate
7Save record to database (transaction not yet committed)Record now has an Id, but rollback can still happen
8Execute all After TriggersRecord has an Id; you can create related records here
9Execute Assignment RulesLead/Case assignment rules change Owner
10Execute Auto-Response RulesCase/Lead automatic reply emails
11Execute Workflow RulesIf a Field Update exists, re-fires steps 4–8
12Execute Escalation RulesCase escalation rules
13Execute Process Builder and Flows launched by Process/WorkflowProcess Builder is retired, but legacy orgs still have them
14Execute After-Save Record-Triggered FlowsCan perform DML, but watch for recursion
15Execute Entitlement RulesService Cloud specific
16Calculate Roll-Up Summary fields on parent recordsTriggers the parent record's save process
17If cross-object field updates exist, repeat the parent record's save logicGrandparent records can also be affected
18Execute Criteria-Based Sharing evaluationSharing rules are recalculated
19Commit all DML operations to the databaseTransaction is finalized; any prior exception rolls back everything
20Execute post-commit logic: send emails, async Apex (Future/Queueable), async Flow pathsEmails and async tasks only execute after commit

Four Critical Phases Unpacked

Phase 1: Before-Save Flow vs Before Trigger — Which Runs First?

Many developers assume before triggers are the earliest point where you can touch a record. Since Spring '22, that's wrong. Before-Save Record-Triggered Flows run before before triggers (step 3 vs step 4). If a Flow modifies a field value, the before trigger receives the already-modified version.

Practical impact: if a Flow changes Status from "New" to "In Progress," your before trigger's if (Trigger.new[0].Status == 'New') condition will never match. If you only inspect the trigger code during debugging, you'll never find the cause.

Before-Save Flows also share a key trait with before triggers: modifying the current record's fields requires no DML — you just assign values directly. This makes them more performant than After-Save Flows for simple field updates.

Phase 2: Why Validation Rules "Sometimes Don't Fire"

Validation Rules execute at step 5 — after before triggers. This creates a common confusion: if your before trigger sets a field to a valid value, the Validation Rule won't complain. Conversely, if the trigger sets an invalid value, the Validation Rule catches it and blocks the save.

Here's the counterintuitive part: Validation Rules only run once per transaction. When a Workflow Field Update in step 11 re-fires triggers, Validation Rules do not run again. This means a Workflow can write "invalid" values to the database, completely bypassing your Validation Rules.

// before trigger: force Amount to a negative value
trigger OpportunityTrigger on Opportunity (before update) {
    for (Opportunity opp : Trigger.new) {
        opp.Amount = -100; // Validation Rule will catch this after the trigger
    }
}

// But if this value is set by a Workflow Field Update,
// the Validation Rule won't re-run, and -100 goes straight to the database

Phase 3: The Workflow Field Update "Double Fire" Trap

Step 11 is the most bug-prone part of the entire execution order. When a Workflow Rule includes a Field Update, Salesforce sends the updated record back to step 4 for another round of before triggers → after triggers. This only happens once — it won't loop infinitely.

Back to the opening example: the Opportunity trigger checks Trigger.old.StageName != Trigger.new.StageName to detect stage changes. First execution works fine — one Order created. But after the Workflow updates an unrelated field and re-fires the trigger, Trigger.old still holds the original values from before the initial update, not the intermediate state after the Workflow. The stage comparison matches again, and a duplicate Order is created.

// Buggy version
trigger OpportunityTrigger on Opportunity (after update) {
    List<Order> orders = new List<Order>();
    for (Integer i = 0; i < Trigger.new.size(); i++) {
        if (Trigger.new[i].StageName == 'Closed Won'
            && Trigger.old[i].StageName != 'Closed Won') {
            orders.add(new Order(
                AccountId = Trigger.new[i].AccountId,
                OpportunityId = Trigger.new[i].Id,
                Status = 'Draft',
                EffectiveDate = Date.today()
            ));
        }
    }
    if (!orders.isEmpty()) insert orders;
}

// Fixed version: use a static Set to prevent double execution
public class TriggerControl {
    public static Set<Id> processedOppIds = new Set<Id>();
}

trigger OpportunityTrigger on Opportunity (after update) {
    List<Order> orders = new List<Order>();
    for (Integer i = 0; i < Trigger.new.size(); i++) {
        if (Trigger.new[i].StageName == 'Closed Won'
            && Trigger.old[i].StageName != 'Closed Won'
            && !TriggerControl.processedOppIds.contains(Trigger.new[i].Id)) {

            TriggerControl.processedOppIds.add(Trigger.new[i].Id);
            orders.add(new Order(
                AccountId = Trigger.new[i].AccountId,
                OpportunityId = Trigger.new[i].Id,
                Status = 'Draft',
                EffectiveDate = Date.today()
            ));
        }
    }
    if (!orders.isEmpty()) insert orders;
}

A common but flawed approach is using a static Boolean for recursion control. In bulk operations (say, Data Loader updating 327 records), Salesforce processes them in chunks of 200. The static Boolean flips to true after the first chunk, and the remaining 127 records have their trigger logic skipped entirely. Using a static Set<Id> to track processed record IDs is the correct pattern.

Phase 4: After-Save Flow vs After Trigger Execution Order

After Triggers (step 8) run before After-Save Flows (step 14). Here's how they compare:

DimensionAfter TriggerAfter-Save Flow
Execution timingStep 8Step 14
Modify current recordNot directly (requires extra DML)Yes, via Update Records element
Create/update related recordsVia DMLVia Create/Update Records elements
Execution order controlNon-deterministic for multiple triggersConfigurable via Flow Trigger Order (1–2000)
Recursion riskRequires manual controlPlatform auto-limits (each Flow runs once per record)

Since Spring '22, Record-Triggered Flows support a Trigger Order number (1–2000). Flows numbered 1–1000 execute first (ascending), followed by unnumbered Flows (non-deterministic order), then 1001–2000. This solves the long-standing problem of unpredictable execution order when multiple Flows target the same object.

Trigger.old Behavior During Re-Execution

This deserves its own section because it trips up so many developers.

During a normal update, Trigger.old contains the field values from the database before the update, and Trigger.new contains the values about to be written. Straightforward.

But when a Workflow Field Update triggers re-execution, Trigger.old does not contain the intermediate state after the Workflow update. It holds the original values from before the entire transaction started. The official documentation states:

Trigger.old contains a version of the objects before the specific update that fired the trigger. However, there is an exception. When a record is updated and subsequently triggers a workflow rule field update, Trigger.old in the last update trigger doesn't contain the version of the object immediately before the workflow update, but the object as it existed before the initial update was made.

In plain English: no matter what happens in between, Trigger.old always anchors to the version at the very start of the transaction. Keep this in mind whenever you're doing field-change detection.

Controlling Record-Triggered Flow Execution Order

When multiple Record-Triggered Flows exist on the same object, the default execution order is non-deterministic. Since Summer '22, Salesforce supports Flow Trigger Order — a number you set in the Flow's Start element:

  • Flows numbered 1–1000 execute in ascending order
  • Unnumbered Flows execute in the middle (order not guaranteed)
  • Flows numbered 1001–2000 execute last

In Setup → Flows → Flow Trigger Explorer, you can view the execution order of all Record-Triggered Flows for any object. This tool is invaluable when troubleshooting automation conflicts.

Debugging Checklist

When you encounter unexpected field values or logic firing when it shouldn't, work through this sequence:

StepWhat to CheckTool
1Is a Before-Save Flow modifying the field?Flow Trigger Explorer
2Is a Before Trigger overwriting the field value?Debug Log (BEFORE_UPDATE events)
3Does a Workflow Rule have a Field Update?Setup → Workflow Rules
4Is the Field Update causing trigger re-execution?Debug Log — count trigger executions
5Is an After-Save Flow updating the current record?Flow Trigger Explorer + Debug Log
6Is a Roll-Up Summary triggering the parent record's save chain?Check triggers and automation on the parent object

In Debug Logs, search for CODE_UNIT_STARTED and CODE_UNIT_FINISHED to see exactly when each trigger, Flow, and Workflow starts and ends. This lets you quickly trace the full execution chain.

Recommendations for New Projects

Salesforce is pushing hard to replace Workflow Rules and Process Builder with Flow. For new projects:

  • If a Before-Save Flow can handle it, skip the Before Trigger — no DML cost, runs earlier, and admins can maintain it
  • Use Apex Triggers for complex scenarios: recursion control, high-volume bulk processing, and callouts
  • Use After-Save Flows instead of Process Builder, but mind the recursion behavior
  • Don't rush to migrate existing Workflow Rules in legacy orgs, but all new automation should use Flow
  • Always set a Trigger Order number on every Record-Triggered Flow — build the habit early

Order of Execution isn't something you memorize and forget. Every time you add a new trigger, Flow, or Validation Rule, mentally walk through all 20 steps. Figure out where your new logic sits in the chain and whether it conflicts with anything already there. That's what it actually means to master the execution order.

Related Articles

Discussion

Ask a Question

Your email will not be published.

No questions yet. Be the first to ask!