GTM Systems
Forty Flows Fight on Every Save and Nobody Knows Who Wins
A lead score kept reverting to zero seconds after it was set. The cause was two record-triggered flows racing on the same save, and no document said which fired first. Here is how to map the firing order of an accreted automation stack and stop the fights.
· 8 min read
A lead score kept setting itself back to zero. A rep would open a lead, watch the score populate at 82, refresh, and see it read 0. It looked like a bug in the scoring flow, so that is where I started, and the flow was correct. The score was correct. The problem was that a second record-triggered flow, built by a different person a year earlier to stamp a default on new leads, also fired on the same save and wrote zero over the top. Two flows, one save, and no artifact anywhere said which one ran last. The org had forty-some record-triggered flows across its objects, all firing on the same handful of saves, and the firing order was whatever Salesforce happened to pick that day.
This is the tax of an automation stack that grew one flow at a time. Each flow was reasonable when someone built it. Nobody ever wrote down the whole set that fires on a given save, so the interactions between them are invisible until one clobbers another in production. The fix is not fewer flows for its own sake; it is knowing and controlling the order they fire in, and documenting it so the next person does not race a flow they never knew existed.
The save order you can control, and the part you cannot
Salesforce runs a fixed order of execution on every save, and you need to know the pieces automation lives in. Before-save flows run early, in the same transaction, cheap and fast, good for setting fields on the record being saved. Then Apex before-triggers, then validation rules, then the save, then after-save flows and Apex after-triggers, then workflow rules, then processes, then anything those trigger, which can loop the whole thing again. Two before-save flows on the same object have a defined ordering you can set with a Trigger Order value. Two after-save flows likewise. What you do not control cleanly is the interaction across the whole zoo when they cascade, which is where the lead score died.
Why the fights are invisible
The reason nobody catches these before production is that each flow is built and tested in isolation. You build the scoring flow, you test it, the score sets correctly, you ship. The default-stamp flow was shipped a year earlier and passed its own tests. Neither test exercised the two flows firing on the same save, because no one knew both existed on that object. The interaction is an emergent property of the whole stack, and the stack is documented nowhere. The count of possible interactions grows with the square of the flows on an object, which is why an org feels fine at five flows and chaotic at forty.
View as table
| Item | Value |
|---|---|
| 5 flows | 10 pairs |
| 10 flows | 45 pairs |
| 15 flows | 105 pairs |
| 20 flows | 190 pairs |
Map the stack, then set the order
The first move is not to change anything. It is to inventory every automation that fires on the object and write down the trigger, the timing, and the fields each one writes. Tooling API gives you the flow definitions; a metadata pull gives you the trigger conditions. Once you can see the whole set that fires on a save, the fights become obvious: two flows writing Lead_Score__c, one at 82 and one at 0. Then you decide the order deliberately. Salesforce lets you set a Trigger Order on record-triggered flows in the same before/after slot; lower numbers run first.
<!-- Lead_Set_Default_Score.flow-meta.xml -->
<start>
<triggerOrder>10</triggerOrder> <!-- default stamp runs first -->
</start>
<!-- Lead_Calculate_Score.flow-meta.xml -->
<start>
<triggerOrder>20</triggerOrder> <!-- real score runs after, wins -->
</start>
With the order set, the default stamps first and the real score writes last, so 82 survives. The deeper fix is consolidation: two flows writing one field on one object is usually one flow with two branches. But you cannot consolidate what you have not mapped, so the map comes first.
The worked example: reconstructing the race
Racing flows versus a documented firing order
| Undocumented stack | Documented firing order | |
|---|---|---|
| Which flow runs first | Whatever the platform picks | Set by Trigger Order, deterministic |
| Two flows on one field | Non-deterministic clobber | Ordered, or consolidated to one |
| Finding the interaction | A production bug, weeks later | Visible in the stack map |
| Adding a new flow | Blind to what already fires | Slot it into the documented order |
| Debugging a bad write | Guess across forty flows | Read the map, then the debug log |
Here is how I map and control the firing order
- 1
Inventory every automation on the object
List every record-triggered flow, before and after save, plus any surviving process or Apex trigger. Pull flow definitions from the Tooling API so you have the real set, not the one someone remembers.
- 2
Record trigger, timing, and fields written
For each automation: what save fires it, before or after, and which fields it writes. The map is a table. The fights are the fields written by more than one row.
- 3
Find the collisions
Any field written by two automations on the same timing is a race. Any after-save flow that updates the same object it fires on is a re-entrancy risk. Flag both.
- 4
Set Trigger Order to make it deterministic
Assign explicit Trigger Order values to the flows in each slot so the order is a decision, not a coin flip. This is the fast fix you can ship before refactoring.
- 5
Consolidate toward one flow per object per timing
Fold the racing flows into a single before-save and single after-save flow with ordered branches. Retire the losers. Now there is one place to reason about order.
- 6
Keep the map as a living document
The stack map lives next to the code. Every new flow gets slotted into it before it ships, so the next person is never blind to what already fires.
The lead score was never wrong. The org just had two flows writing it on the same save and no document saying which won, so the answer changed depending on which one the platform ran last. That is the signature of an automation stack that grew by accretion: every flow correct in isolation, the whole set undefined in aggregate. Map what fires on each save, make the order a decision instead of a coin flip, then consolidate the racers into one flow you can reason about. The same accretion shows up as a broader stack you have to pay down, which is automation debt, and the discipline that keeps new automation from adding to the pile is declarative-first with a clear line for code.
Keep reading
One email. Every week.
One email a week: a systems problem I architected or untangled, with the schema, the config, and what I would change. No roundups, no theory, unsubscribe whenever it stops being useful.
The newsletter opens soon.
Connect a provider in src/config.ts