Flow mistakes that only surface under load
The dangerous defects are not the ones that throw errors. They are the ones that pass every test the builder thought to run, then produce a wrong answer quietly on the records nobody clicked through.
Platform & ArchitectureThe flow works. Somebody created a record in the user interface, watched the field populate, took a screenshot, attached it to the ticket and shipped the change. That evidence is real and it is close to worthless, because it establishes behaviour under exactly one condition: a single record, saved by an administrator, with every field filled in.
Production is not that. Production is a data load of several thousand records, an integration user writing overnight, a record where a field the builder assumed would be populated is blank, and two other automations on the same object working in the same transaction. The flow that passed the screenshot test can be wrong in all four and report nothing at all.
That is the class of mistake worth writing down. Not the flow that errors, which announces itself and gets fixed the same week, but the flow that succeeds and is wrong. The sections below are organised by the condition that exposes each fault, because the element it lives in is rarely what the builder was missing. The condition they never tested is.
A flow can be wrong without ever failing
Two failure modes wear very different clothes. The first throws an error: somebody sees a message, the save is blocked, a ticket is raised, and within a day or two the cause is found. Unpleasant and self-correcting.
The second returns a value. The wrong value, on some records and not others, with no error and no interruption to anybody's day. The field is populated, the record saves, the flow reports success. Six weeks later a report does not reconcile, and the investigation starts from the report rather than from the automation, which is why these take months to find rather than days.
A failing flow is discovered by the platform. A wrong flow has to be discovered by a person noticing, and people notice slowly, if at all, when the number on the screen is plausible.
The conditions that expose each mistake
The centre column is the point of the table. It is the condition under which the fault becomes visible, and it is almost never the condition the flow was built against.
| Mistake | Condition that exposes it | Test before it ships |
|---|---|---|
| Logic built on one record, then applied to a collection | A data load or a mass update | Run a mixed batch through the same save path in a sandbox |
| A retrieval whose filter does not guarantee a single match | Any record with a second match | Create a duplicate deliberately, check which one the flow used |
| A value calculated once and reused inside a loop | More than one iteration | Run the loop over three unlike records and read the debug output |
| No fault path on a create, an update or an action | A validation rule, a required field, an unavailable service | Force the failure with a temporary validation rule |
| A comparison that assumes a field is populated | A legitimate record with that field blank | Save a record with the field cleared |
| A count or a total taken from an empty collection | A retrieval that found nothing | Run it against a parent record with no children |
| Logic that depends on which automation writes first | A second flow or trigger on the same object | List every automation on the object and the fields each one writes |
| A flow built and tested as an administrator | An integration user or a restricted profile | Re-run the same action as the least privileged user who triggers it |
None of the tests in the right-hand column is expensive. Each costs an afternoon the first time and minutes on every build after that, which is the argument for running them as a checklist rather than as an investigation after something has gone wrong.
The record the user did not touch
A record-triggered flow does not run once per user action. It runs once per record in the transaction, and a data load puts many records into one transaction. The logic inside has to be correct for every one of them independently.
The correctness version of this fault is different from the performance version, and separating them saves days. Performance faults make the transaction slow or push it into a limit, which is loud. Correctness faults give every record in the batch the same answer, which is silent. If the symptom is a slow save or a limit error rather than a wrong value, the diagnosis you want is in Why your flow is slow, and the two investigations share almost no evidence.
The shape to look for is a value established once and then used inside a loop as though it belonged to the current iteration. A variable assigned before the loop begins. A record retrieved outside the loop and read inside it without being matched back to the item being processed. On one record, the value established once is the value for that record, so the flow is right. On two hundred, every record gets the first record's answer, and every one of them saves cleanly.
The confirmation is a batch, not a bigger batch. Three records is enough, provided they are unlike each other in the ways the logic cares about: different owners, different record types, different values in the field the decision reads. Three identical records confirm nothing, because they cannot distinguish a correct flow from one that answered once and reused the answer.
The blank nobody expected
Almost every wrong-answer flow we have unpicked contained a comparison assuming a field would be populated. The assumption was reasonable when it was made, because the field is populated on every record anybody looks at.
Then a legitimate blank arrives. An integration writes a record without the optional field. A user clears a value. A retrieval that found nothing returns an empty collection rather than an error, and the flow carries on down the branch built for the case where something was found.
How an absent value behaves is not intuitive and it is not identical in every element, which is precisely why it should be tested rather than reasoned about. Rather than reconstruct the rules from memory, check the current behaviour in the Salesforce developer documentation and confirm it in a sandbox against a real blank record. The documentation tells you the rule. The sandbox tells you what your arrangement of elements does with it.
Three habits remove most of this class. Decide explicitly what should happen when a retrieval finds nothing, and build that branch rather than letting the flow fall through it. Check whether a collection contains anything before doing arithmetic on its contents. And be deliberate about negative conditions, because a condition written as not equal to behaves differently against a blank than most builders expect, and that single asymmetry accounts for a surprising share of records sent down the wrong branch.
The retrieval that assumed one answer
A flow retrieves a record using a filter. On the builder's data that filter matches once. In production it matches twice: there is a duplicate, two cases are open on the same account, a merge left something behind, or two contacts share an email address.
When more than one record matches and the flow is configured to take a single one, the platform returns one of them. The flow does not stop, does not warn, and gives no indication that a choice was made on its behalf. Everything downstream proceeds on the assumption that the returned record was the only one.
This is the least visible fault on the list and the most common cause of a value that is right on most records. The confirmation is direct: create the duplicate deliberately in a sandbox, run the flow, check which record it used.
The fix is a decision rather than a setting. Either the filter is made genuinely unique, which is the honest answer wherever a unique key exists, or the flow retrieves every match and handles the multiple explicitly: sort deliberately, take the record the business rule actually wants, and treat more than one match as a case that needs an owner rather than a case that needs a guess.
The ordering that held until it did not
Any flow that takes the first record from a retrieval has made an ordering assumption. If no sort was specified, the order was never guaranteed, and the flow has been correct by coincidence for however long it has been running.
Coincidence is durable enough to be dangerous. Records come back in an order resembling creation order for months, then a load reorders data or platform behaviour moves with a release, and the same flow starts choosing a different record. Nothing anybody can point to changed in the org, which makes the investigation far harder than it should be.
Two disciplines close it. Specify the sort whenever a flow takes one record from a set, even where the current order happens to be the one you want, because writing it down converts a coincidence into a requirement the next person can check. And treat releases as a source of behavioural change rather than only of new features, which is the argument for triaging each release deliberately.
The user who is not you
The flow was built by an administrator, tested by an administrator and demonstrated to the business by an administrator. Every one of those runs had access to every record, every field and every picklist value in the org.
The people and systems that trigger it in production do not. An integration user carries a narrow profile. A portal user sees a subset of records. A sales user may not have access to the field the flow reads to make its decision.
The failure mode is instructive because it is usually not an error. A flow running in the context of a user who cannot see a record does not necessarily announce that fact. It retrieves nothing, treats nothing as an ordinary empty result, and proceeds down the branch built for no match found. The record existed. The user could not see it. The flow was, from its own point of view, entirely correct.
The test is to re-run the same action as the least privileged user who genuinely triggers it, in a sandbox, with that user's real profile and permission sets rather than an approximation. Where a flow legitimately needs to act beyond the triggering user's access, that is a deliberate design decision with a security consequence attached, and the architect guidance on running context is the reference to read before changing it rather than after.
The flow that assumed it was alone
The last condition is the hardest to test for, because it is not a property of your flow at all.
Your flow reads a field, makes a decision and writes a result. Another flow, a trigger or a managed package writes that same field in the same transaction. Which value your flow read depends on the order the automation ran in, and that order depends on things neither builder decided.
The symptoms are recognisable once you know the pattern. A result correct sometimes and not others, with no discernible difference between the records. A value that is right when a user saves the record and wrong when an integration does, because the two paths trigger different automation. A field correct until somebody edits the record a second time.
The evidence is a list rather than a test. For the object in question, write down every automation that runs on save and every field each one writes. Overlap between what your flow reads and what something else writes is the finding, and usually the whole finding. That inventory is tedious to build once and cheap to maintain, and it is one reason naming and documentation conventions earn their keep as an org grows, which we treat separately in Flow in a large org: conventions that hold at a hundred flows.
The half-finished transaction
One structural mistake sits underneath several of the above: no fault path.
A create, an update or an action can fail for reasons your flow did not cause. A validation rule fires. A required field is empty on a record the flow is assembling. A service the action calls is unavailable. Without a fault path the flow stops where it stopped, and what happened before that point may already be committed.
The result is a half-finished piece of work: a parent record updated and the child never created, a status advanced and the notification never sent. Nobody is told, because from the user's side the save either appeared to succeed or produced a message reading as a platform error rather than a business problem.
Build the fault path, and make it do two things. Record what failed somewhere a person will actually read it, with the record identifier included, because a fault path writing to a log nobody opens is decoration. And leave the data in a state somebody can reason about, which sometimes means reversing the earlier step and sometimes means flagging the record for attention, but never means continuing silently as though the step had worked.
Testing for conditions instead of the happy path
None of this requires a testing framework, and that is the point. It requires five fixtures kept in a sandbox and reused on every build.
A batch of three unlike records, pushed through the same save path production uses. A record with the optional fields deliberately cleared. A deliberate duplicate for anything the flow retrieves through a filter that is not unique. A login as the least privileged user who triggers the flow, and separately as the integration user. A temporary validation rule to force a failure and prove the fault path fires.
Build those once per object and the marginal cost per flow is close to nothing. The platform gives you every one of these conditions in a sandbox at no cost, so the reason they get skipped is never expense. It is that the happy path passed and nobody asked what else was true.
Then write down what you tested, in the flow description or the ticket, wherever your team actually reads. The next person to inherit this automation will otherwise repeat the whole investigation from the same starting point you did: a flow that works, a screenshot that proves it, and no record of the conditions under which either statement holds.



