Blog

Salesforce order of execution: test it, don’t memorise it

The Salesforce order of execution on insert, from before-save flows to after-save flows, confirmed by a local Apex test in milliseconds instead of a debug log after a deploy.

Short version

The Salesforce order of execution is the fixed sequence a record save goes through: on insert, before-save flows, before triggers, validation rules, duplicate rules, the save, after triggers, assignment rules, workflow rules, processes, after-save flows. renmy runtime, a local Salesforce Apex runtime, runs your triggers, validation rules and Salesforce Flows in those slots on your own machine against an embedded PostgreSQL, with no org, so an Apex test that inserts a record and asserts its final state confirms the order in milliseconds instead of a debug log after a deploy. Two assumptions cause most of the bugs: that a before trigger can see what an after-save flow sets, and that a before-save flow and a before trigger can both own the same field.

renmy runtime was called Nimbus until October 2026. Same tool, same binary, new name; the record of the rename has the details.

Who this is for. Apex developers whose objects also carry record-triggered flows, and admins who build flows on objects that already have triggers. The admin’s side, with no terminal assumed, is in Test Salesforce Flows locally before you activate them.

What is the Salesforce order of execution?

When you insert a record, Salesforce does not run “the trigger” and then “the flow”. It runs a fixed list of steps, each in its own slot, and your automation fires in whichever slot it belongs to. On insert, the slots are:

  1. before-save: before-save record-triggered flows (flows/)
  2. before: before triggers (triggers/)
  3. validate: validation rules (validationRules/, parent fields included)
  4. duplicate: duplicate rules (duplicateRules/ and the standard rules)
  5. commit: the record is saved to the database
  6. after: after triggers (triggers/)
  7. assign: assignment rules (assignmentRules/, when the DML asks for them)
  8. workflow: workflow rules (workflows/)
  9. process: processes from Process Builder (flows/, processType Workflow)
  10. after-save: after-save record-triggered flows (flows/)

The folder names are where each piece lives in a Salesforce DX project. Salesforce’s own list is longer and covers every DML operation; these ten are the slots the flows page documents for an insert, and the ones this article uses.

The two mistakes behind most order-of-execution bugs

Mistake one: expecting the before trigger to see what the after-save flow sets

An admin builds an after-save flow on Account: when AnnualRevenue is above one million, set Tier__c to Enterprise. A developer writes a before-insert trigger that reads Tier__c and sets SLA_Days__c to 5 for Enterprise accounts and 30 for everyone else. Each works on its own. Together, every new Enterprise account gets a 30-day SLA.

The trigger ran in the before slot, with Tier__c still null. The flow set Tier__c in the after-save slot, eight slots later. Nothing failed, nothing was logged, and the data is wrong.

Mistake two: a before-save flow and a before trigger both writing the same field

An admin builds a before-save flow: when BillingCountry is DE, set Industry to Technology. Years earlier, a developer wrote a before-insert trigger that defaults Industry to Manufacturing for any account whose website ends in .de. Insert a German account with a .de website and both fire: the flow in the before-save slot, the trigger in the before slot. The trigger writes last, so Manufacturing is what the validation rules see and what gets saved. The admin’s flow is active, correct, and overwritten on every record it touches.

Either fix is a one-line change once you know which slot ran when. Knowing is the expensive part.

How you confirm the order today: deploy, then read a debug log

The current method: set a trace flag, deploy the trigger and the flow to a sandbox or scratch org, insert a record by hand, download the debug log, and read it for the lines where each code unit and each flow interview starts. The log tells you the order for that one record, in that one org, on that day.

It takes about four minutes from saving the trigger to reading the log, and the log is stale the moment you change a line. You are not testing the order of execution; you are observing it once and hoping nothing moves. It works this way because a Salesforce developer has never had a runtime on their own machine: the Apex compiler, the flow engine and the database all live in the org, so the org is the only place a save can happen.

A Java or JavaScript developer presses “run test” and a program on their laptop answers in under a second. That is what a local runtime is: the thing that executes your code, running on your machine instead of on a server you deploy to first.

Why a local runtime can run the order of execution on your laptop

renmy runtime is a local Salesforce Apex runtime and developer toolchain. It executes real Apex (classes, interfaces, enums, generics, triggers), SOQL and DML, and Salesforce Flows (record-triggered, autolaunched and screen, with subflows) on your machine against an embedded PostgreSQL. No Salesforce org, no scratch org, no sandbox, no Docker, no JVM: one binary on macOS, Linux or Windows. It is not a mock and not a transpiler. Governor limits are enforced, Test.startTest/stopTest and @testSetup behave as in the org, and the parity record of what matches and what does not is public.

For the order of execution, the relevant part is this: renmy runtime reads the .trigger files, the .flow-meta.xml files and the validation rules from your SFDX project, and when a test performs DML it runs each of them in the slot listed above. Flows fire when DML fires, as in production, with no annotation or separate setup. A flow fault reaches your Apex as the exception Salesforce throws: a DmlException with FIELD_CUSTOM_VALIDATION_EXCEPTION for a Custom Error element, CANNOT_EXECUTE_FLOW_TRIGGER for anything else.

Instead of a log that says what happened once, you write a test that says what must happen, and run it on every change.

Test the order of execution with one Apex test

Here is the project with both mistakes in it.

bash
force-app/main/default/
├── flows/
│   ├── Account_Before_Save.flow-meta.xml   # before-save: BillingCountry = DE -> Industry = Technology
│   └── Account_Tier.flow-meta.xml          # after-save:  AnnualRevenue > 1000000 -> Tier__c = Enterprise
├── triggers/
│   └── AccountTrigger.trigger              # before insert
├── objects/Account/validationRules/
│   └── Region_Required.validationRule-meta.xml
└── classes/
    └── AccountOrderOfExecutionTest.cls

The trigger

apex
trigger AccountTrigger on Account (before insert) {
    for (Account acc : Trigger.new) {
        acc.Region__c = (acc.BillingCountry == 'DE') ? 'EMEA' : 'Other';

        // Reads a field the after-save flow has not set yet.
        acc.SLA_Days__c = (acc.Tier__c == 'Enterprise') ? 5 : 30;

        // Legacy default. The before-save flow already set Industry for DE accounts.
        if (acc.Website != null && acc.Website.endsWith('.de')) {
            acc.Industry = 'Manufacturing';
        }
    }
}

The test

The test does not call the trigger or the flow. It inserts one record and asserts the state that record should be in once all ten slots have run.

apex
@IsTest
private class AccountOrderOfExecutionTest {
    @IsTest
    static void insertRunsFlowsAndTriggersInTheirSlots() {
        Account acc = new Account(
            Name = 'Acme GmbH',
            BillingCountry = 'DE',
            Website = 'https://acme.de',
            AnnualRevenue = 5000000
        );

        insert acc;   // before-save flow, before trigger, validation rules, save, after-save flow

        Account saved = [
            SELECT Industry, Region__c, Tier__c, SLA_Days__c
            FROM Account
            WHERE Id = :acc.Id
        ];
        Assert.areEqual('Technology', saved.Industry, 'before-save flow sets Industry for DE');
        Assert.areEqual('EMEA', saved.Region__c, 'before trigger sets Region');
        Assert.areEqual('Enterprise', saved.Tier__c, 'after-save flow sets Tier');
        Assert.areEqual(5, saved.SLA_Days__c, 'Enterprise accounts get a five-day SLA');
    }
}

Read it as a statement of who owns what: the before-save flow owns Industry, the trigger owns Region, the after-save flow owns Tier, and the SLA depends on Tier being known in time. The validation rule that requires Region__c passes without the test setting it, because the validate slot runs after the before slot that filled it in.

Run it

bash
renmy test AccountOrderOfExecutionTest

The first assertion fails:

text
Assertion Failed: before-save flow sets Industry for DE: Expected: Technology, Actual: Manufacturing

Mistake two, found in milliseconds. Delete the legacy default from the trigger, or guard it with acc.Industry == null, and run again. Now the last assertion fails:

text
Assertion Failed: Enterprise accounts get a five-day SLA: Expected: 5, Actual: 30

Mistake one. Tier__c was null when the trigger ran, exactly as the slot list says. The fix is to get the tier into a slot ahead of the trigger: move the tier assignment into the before-save flow, or let the trigger compute the tier from AnnualRevenue itself. Pick one and run again:

bash
renmy test AccountOrderOfExecutionTest
# ✓ AccountOrderOfExecutionTest.insertRunsFlowsAndTriggersInTheirSlots 21ms

A typical Apex test completes in tens of milliseconds, SOQL and DML included; the published benchmark puts four open-source suites, 837 tests, at about 1 min 46 s in an org against 2.4 s locally. Three runs took less time than one trace flag, and the test stays in the project as the record of which slot owns which field.

To see the null rather than infer it, put a breakpoint on the line that reads Tier__c and run the test under the debugger: a live DAP session in VS Code or IntelliJ IDEA (the JetBrains plugin is Renmy Runtime - Salesforce IDE) pauses inside the trigger with Trigger.new in the variables pane. Stepping is Pro, free during the beta.

If the object has several triggers, the order between them is a separate question, covered in Do you still need an Apex trigger framework?

The whole chain shares one set of governor limits

Everything in those ten slots runs in one transaction, under one set of governor limits: 100 SOQL queries, 150 DML statements, a heap ceiling. A trigger that runs one query per record looks fine in a test with 10 records and throws System.LimitException in a batch of 200. renmy runtime enforces the same limits, with the same counts and the same errors, so that trigger fails at 101 queries on your machine as it does in the org. renmy.governor.mode decides what a violation does: strict fails the test, warn prints a warning and continues, off tracks nothing. The usual arrangement is warn locally and strict in CI, declared once in the committed renmy.properties and selected with NIMBUS_PROFILE=ci:

properties
# renmy.properties

# Local: warn on violations, stay unblocked
renmy.governor.mode=warn

# CI: strict enforcement, fail on any violation
%ci.renmy.governor.mode=strict

The governor limits page has the full property list.

I have a sandbox. Why not just read the log?

Because the log answers a different question. A debug log tells you what the order of execution did to one record, once, after a deploy. A test tells you what it must do to every record, every time you save a file, and fails the moment someone’s new flow or trigger breaks the arrangement. The sandbox is still where you activate the flow, where a user clicks through it and where the business signs off. What moves to your machine is the part you used to do by reading logs.

Try it

bash
# macOS, Linux
curl -fsSL https://renmy.net/install.sh | sh

# Windows PowerShell
irm https://renmy.net/install.ps1 | iex

cd your-sfdx-project
renmy test "*"

The quickstart covers the first run in under five minutes. Issues, docs and released binaries are on GitHub; for IntelliJ IDEA, the plugin is Renmy Runtime - Salesforce IDE. The Free tier covers unlimited local test runs on one machine, flows and triggers included, with no account and no card.

Frequently asked questions

What is the Salesforce order of execution on insert?

The fixed sequence of slots a record save goes through. On insert: before-save record-triggered flows, before triggers, validation rules, duplicate rules, the save to the database, after triggers, assignment rules, workflow rules, processes from Process Builder, and after-save record-triggered flows. Each piece of automation fires in its own slot.

Does a before trigger see a field set by an after-save flow?

Not on the same save. The before trigger runs in the before slot and the after-save flow runs in the after-save slot at the end of the list, so the field is still at its original value while the trigger runs. If the trigger needs the value, it has to be set in an earlier slot: by a before-save flow, or by the trigger itself.

Which wins when a before-save flow and a before trigger write the same field?

The trigger. Before-save flows run in the first slot and before triggers in the second, so the trigger writes last and its value is what the validation rules check and what is saved. If the flow is meant to own the field, the trigger has to stop writing it, or write it only when it is empty.

Can I test the Salesforce order of execution without an org?

Yes. renmy runtime, a local Salesforce Apex runtime, reads the triggers, flows and validation rules from your Salesforce DX project and runs them in the documented slots on your machine, against an embedded PostgreSQL. An Apex test that inserts a record and asserts its final state runs in tens of milliseconds, with no deploy, no debug log and no org.

Does renmy runtime run validation rules and duplicate rules locally too?

Yes. On insert it runs validation rules, parent fields included, after the before triggers, then the duplicate rules from your project together with the standard rules, before the record is saved to the embedded database. Governor limits are enforced in the same transaction, with the same counts and exceptions as Salesforce. The parity record on renmy.net lists what matches and what does not.

Run the order of execution on your machine

renmy runtime fires your before-save flows, triggers, validation rules and after-save flows in their slots, inside your Apex tests, against an embedded PostgreSQL. No org, no deploy, no debug log.