The Salesforce order of execution is the fixed sequence of events that runs every time a record gets saved, whether that save comes from a user clicking a button, an API call, or a batch job. It decides whether your validation rule fires before or after your flow, whether your trigger sees the old value or the new one, and why a single save can sometimes run your before-trigger logic twice. Miss this sequence and you will spend hours debugging a bug that was never in your code.
Most admins learn Salesforce backwards. They build a flow, then a validation rule, then a trigger, and assume everything runs in the order they built it. It doesn't. Salesforce runs a rigid, documented sequence regardless of build order, and that sequence is the same whether you're saving one record or fifty thousand through a data load.
Here's what actually happens between the moment a record is saved and the moment it lands in the database, for an update to an existing record.
| Step | What Runs |
|---|---|
| 1 | Original record loaded from database, new values applied in memory |
| 2 | System validation (required fields, field formats, field-level security) |
| 3 | Before-save record-triggered flows |
| 4 | Before triggers (old and new values available) |
| 5 | Custom validation rules |
| 6 | Duplicate rules |
| 7 | Record saved to database, not yet committed |
| 8 | After triggers |
| 9 | After-save record-triggered flows |
| 10 | Assignment rules, auto-response rules |
| 11 | Workflow rules (if a field update fires, steps 2 through 10 run again) |
| 12 | Processes and flows launched by that workflow field update |
| 13 | Entitlement rules, roll-up summary recalculation on parent records |
| 14 | Post-commit: outbound messages, email alerts, future methods, queueable jobs |
Notice step 11. That line is the reason your trigger fires twice, and it's the single most misunderstood part of the whole sequence.
A workflow rule with a field update does not just update a field quietly in the background. It forces the record through validation and before/after triggers a second time, in the same transaction. If your before-trigger logic isn't written to handle a second pass, you'll get duplicate task creation, doubled email sends, or a recursion error that crashes the save entirely.
This isn't a bug. It's documented behavior that's existed since workflow rules shipped. The fix is boring but effective: check whether the triggering field update actually happened before running expensive logic, and use a static boolean flag in a class to block a trigger from re-entering itself when it doesn't need to.
Record-triggered flows with field updates on the same object cause an identical problem, though with one safety net Salesforce added later: flow has a hard limit of recursion depth (typically capped before it throws a runtime exception), so infinite loops get stopped automatically. Workflow rules have no such ceiling. An old workflow rule updating its own triggering field can loop until it hits governor limits and fails loudly.
Salesforce added before-save record-triggered flows specifically to compete with before triggers on performance, and the order matters. Before-save flows run first, then before triggers run second, both operating on the same in-memory record before it touches the database.
This creates a real design choice. If a flow sets a field value and a trigger also sets that same field, the trigger's value wins, because it runs later in the sequence and overwrites whatever the flow did. Teams that don't know this spend days debugging why a flow-calculated field keeps reverting to a trigger's hardcoded default.
The practical rule: pick one tool per field. If a trigger owns a field's logic, don't also let a flow touch it on the same object and same save event. Splitting ownership across two automation tools on the same field is how technical debt gets built one well-intentioned change at a time.
Validation rules run after before triggers, not before them. That means a before trigger can modify a field value, and the validation rule will check the modified value, not the one the user originally entered. This is useful: a trigger can clean up bad data before the validation rule ever sees it.
It also means validation rules can't stop a before trigger from running its logic first. If your before trigger does something destructive before a validation rule blocks the save, that destructive action already happened in memory, even though the record never commits to the database. Most of the time this doesn't matter because nothing persisted. But if that trigger logic calls an external system via a synchronous callout, or queues a future job, that side effect already fired, independent of whether the save ultimately succeeds or fails.
After triggers run once the record is written to the database but before the transaction commits. They can query the record's own ID and related records that were just inserted, which before triggers cannot do reliably for new records. This is why anything involving parent-child relationship updates, roll-up logic the platform doesn't handle natively, or related record creation almost always belongs in an after trigger.
Future methods, queueable jobs, and outbound messages wait until after the entire transaction commits successfully. If the transaction rolls back for any reason, including a validation failure three steps earlier, none of that asynchronous work ever fires. This is actually a feature: it stops you from sending a confirmation email for a record that technically never got saved.
Three habits prevent 90% of order-of-execution bugs. First, pick one automation tool per object per responsibility, and document which tool owns which field. Second, avoid workflow rules with field updates entirely on new builds; record-triggered flows with before-save updates do the same job without the double-fire risk on recursion. Third, when you inherit an org with workflow rules, triggers, and flows all touching the same object, map the actual execution order before changing anything, because the bug you're chasing is probably a sequencing collision, not broken logic.
Salesforce publishes this order of execution in its own documentation and it hasn't changed in any way that breaks existing automation in years. The platform is predictable. The chaos comes from builders who stack five automation tools on one object without ever checking which one runs first.
This almost always happens because a workflow rule's field update forces the record back through validation rules and triggers a second time in the same transaction. Record-triggered flows with field updates can cause the same effect, though Salesforce caps flow recursion depth automatically. The fix is to check whether a relevant field actually changed before running expensive logic again, or to block re-entry with a static flag in Apex.
Before-save record-triggered flows run before before-triggers on the same save event. After-save record-triggered flows run after after-triggers have finished. If both a flow and a trigger write to the same field during a before-save event, the trigger's value wins because it executes later in the sequence.
Validation rules run after before-triggers execute but before the record is written to the database. That means a before-trigger can modify field values, and the validation rule evaluates those modified values rather than the original user input. Validation rules also run a second time if a workflow field update triggers a re-save cycle.
Before triggers run while the record still exists only in memory, before it's written to the database, so they're used for setting or correcting field values on the record itself. After triggers run once the record has a database ID, which makes them the right place for updating related or parent records and for any logic that needs the record's own ID.
Use a static Boolean variable in an Apex class to track whether the trigger has already run in the current transaction, and skip the recursive logic on the second pass. Also check whether the specific field your logic depends on actually changed, using Trigger.old versus Trigger.new, instead of re-running the full logic block on every invocation regardless of what changed.