The Execution Model
Everything a flow does at run time comes from one small loop: pick a node, run it, look at what it returned, follow an edge to the next node. Once you can predict that loop, you can predict your flow.
This page explains the loop exactly as the runtime implements it. If a flow behaves in a way that surprises you, the answer is almost always on this page.
Purpose
Understanding the execution model lets you:
- Predict which node runs next, without running the flow.
- Tell the difference between "my flow failed" and "my flow took the other branch" — these look similar in a log but mean opposite things.
- Know why a run stopped, and which of the three guardrails stopped it.
- Wire error paths deliberately instead of by trial and error.
When you need this
Read this page before you build anything with branching, loops, or error handling. You need it when:
- A node "succeeded" but the flow went somewhere unexpected.
- A run ends immediately with no obvious error.
- You are deciding whether to handle a problem with retry, with a failure edge, or with an If.
You do not need this page to build a single straight line of Clicks and Waits.
One node at a time
A running flow is a session. Each session executes exactly one node at a time and holds one "current node" pointer.
This matters more than it sounds. It means:
- There is no background node. Nothing else in your flow is running while a node runs.
- A slow node blocks its whole session. A
Find Imagewith a 30-second timeout stops that session for up to 30 seconds. - Parallelism comes from running more sessions (more instances), not from splitting one flow.
To do two things at once, run the flow on two instances, or use the Flow Queue to run several flows across your instance pool. Within a single session, execution is strictly sequential.
The loop
This is the runtime's main loop, in the order it actually runs:
Two consequences are worth memorising:
fatalandcancellednever follow an edge. Wiring aFatalport does not create a recovery path during a normal run — the runtime finalizes the moment it sees that status. Usefailurefor anything you intend to recover from.- A route with no edge ends the run. If a node returns
failureand nothing is wired to its Failure port, the run finalizes asfailurewithTRANSITION_NOT_FOUND. It does not silently continue.
Status versus route — the distinction that matters most
Every node returns two separate things:
| What it describes | Who reads it | |
|---|---|---|
status | Execution health — did the node itself work? | The runtime, for guardrails and retry |
route | Which port the flow leaves through | Edge resolution, to pick the next node |
They are not the same value, and for detection nodes they deliberately disagree.
The five statuses
| Status | Meaning | Retryable | Follows an edge |
|---|---|---|---|
success | Node completed normally | — | Yes |
failure | Recoverable failure | Yes | Yes |
timeout | Expected condition not met in time | Yes | Yes |
cancelled | Run was stopped mid-node | No | No — finalizes |
fatal | Unrecoverable system or configuration error | No | No — finalizes |
Why "not found" is not a failure
This is the single most common source of confusion, so it is worth stating plainly.
When Find Image searches for a template and the timeout expires with no match:
- the status is
success— the detection ran correctly, it simply did not find the image; - the route is
failure— so the flow leaves through the Failure port.
The practical effect: a "not found" never counts toward the consecutive-failure guardrail and never trips node retry. A flow that polls for a rare event a thousand times will not be killed for it. That is intentional — "the button isn't there yet" is a normal outcome, not an error.
The same pattern applies to Check Region (passed / not passed) and to Read Text.
For detection nodes, read the ports as the answer to a question — "was it there?" — not as "did this node work?". The node worked either way.
Edge resolution
When a node returns route R, the runtime looks up the edges leaving (thisNode, R):
- Candidates are pre-indexed at compile time by
(fromNodeId, fromPort). - They are sorted by priority ascending, then by edge id ascending.
- The first candidate with no condition wins.
- A candidate with a condition is evaluated;
trueselects it,falsemoves on. - If a condition errors or returns a non-boolean, that candidate is skipped and the run continues to the next one.
- If nothing is selected, the result is "no edge" — and the run finalizes as
failure.
Edge resolution is pure: it never captures a frame, sends input, changes a variable, or writes a file. It only chooses.
The node attempt pipeline
Inside "run the node", the phases are always in this order:
Points that catch people out:
- If a
{{variable}}inside a parameter cannot be resolved, the node returnsfatalwithPARAM_EXPRESSION_INVALIDbefore any retry happens. A broken expression is a configuration bug, and retrying it would just fail identically. - Output mappings run exactly once, against the final attempt. A node that retries three times does not write its variables three times.
Guardrails
Three limits protect you from a flow that never ends. All three are flow settings, checked by the runtime itself.
| Guardrail | Checked | Effect when exceeded |
|---|---|---|
maxNodeExecutions | Before each node | finalize fatal, MAX_NODE_EXECUTIONS_EXCEEDED |
maxRuntimeDurationMs | Before each node | finalize fatal, MAX_RUNTIME_DURATION_EXCEEDED |
maxConsecutiveFailures | After each node | finalize fatal, MAX_CONSECUTIVE_FAILURES_EXCEEDED |
The consecutive-failure counter behaves like this:
failureandtimeoutincrement it.successresets it to 0.fatalandcancelledfinalize the run before the counter is consulted.
Because a detection "not found" carries status success, it resets the counter rather than incrementing it.
maxNodeExecutions counts every node, including Start and Stop. A flow with a loop that runs 200 iterations over 12 nodes executes ~2400 nodes. The Farm-style flows in this documentation set maxNodeExecutions to 5000 for exactly this reason. If a long flow dies with MAX_NODE_EXECUTIONS_EXCEEDED, raise this value rather than shortening the loop.
Retry backoff time does not count toward an individual attempt's timeout, but it does count toward maxRuntimeDurationMs.
How a run ends
A run finalizes in exactly one of these ways:
| Cause | Final status |
|---|---|
Reached a Stop node | whatever the Stop declares (success / failure / cancelled) |
Reached a Return node | whatever the Return declares |
A node returned fatal | fatal |
A node returned cancelled, or the user stopped the run | cancelled |
| A route had no matching edge | failure (TRANSITION_NOT_FOUND) |
| A guardrail was exceeded | fatal |
A flow that simply "runs off the end" of a wired path does not exist — every path either reaches a terminal node or finalizes with TRANSITION_NOT_FOUND.
Worked example
Consider a small login step:
Trace it when the button appears late:
| Step | Node | status | route | Next |
|---|---|---|---|---|
| 1 | Start | success | success | Find Image |
| 2 | Find Image | success | failure | Wait 2s |
| 3 | Wait 2s | success | success | Find Image (2nd) |
| 4 | Find Image (2nd) | success | success | Click |
| 5 | Click | success | success | Stop |
| 6 | Stop | — | — | finalize success |
Note that step 2 has status success even though it routed to Failure. The consecutive-failure counter never moved, and no retry was involved. The flow simply took its other branch.
Best practices
- Wire the Failure port of every detection node. An unwired Failure port turns "not found" into an ended run.
- Use
failurefor recovery, notfatal.fatalfinalizes immediately and cannot be caught. - Prefer branching over retry for "is it there yet?" Retry is for operations that genuinely failed; a branch is for a screen that is in a different state.
- Give long flows a generous
maxNodeExecutions. It is a safety net, not a budget. - Let
successreset your failure counter. Interleaving successful steps between risky ones naturally keepsmaxConsecutiveFailuresfrom tripping.
Performance notes
- Because a session runs one node at a time, total flow time is the sum of node times. The fastest way to speed up a flow is to shorten timeouts on nodes that usually succeed instantly, not to reorganise the graph.
- A polling node's
timeoutMscovers the whole polling loop — frame capture, cropping, matching, and event emission. Setting a 1-second poll interval inside a 2-second timeout gives you roughly two attempts, not two seconds of matching. - Edge resolution is pure and pre-indexed, so branching itself costs effectively nothing. Do not flatten a readable graph for speed.
Memory notes
- Each session holds its own execution context: variables,
nodeResults, andlastResult. Running the same flow on 10 instances creates 10 independent contexts. nodeResultsgrows with every node executed, because each node's result is recorded for later reference (this is what lets anIfcheck an upstream node's outcome). A flow with a very highmaxNodeExecutionsand thousands of iterations therefore accumulates result data for the lifetime of the run.- Prefer splitting very long-running automation into a Sub Flow called repeatedly, or into scheduled runs, rather than a single session that executes for hours.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Leaving a Failure port unwired | Run ends with TRANSITION_NOT_FOUND | Wire it, even if only to a Stop |
Expecting Fatal to act as a catch-all | Never followed; the run finalizes | Handle recoverable problems on failure |
Adding retry to a Find Image to "look harder" | Retry does not fire — not-found has status success | Raise timeoutMs, or branch on Failure |
| Treating "not found" as an error in logs | Misreads a healthy flow as broken | Check the route, not just the status |
Very long flow with default maxNodeExecutions | Dies mid-run with MAX_NODE_EXECUTIONS_EXCEEDED | Raise the limit in flow settings |
Troubleshooting
The run ended immediately and the log says TRANSITION_NOT_FOUND.
A node returned a route with no edge wired. Find the last node in the log and check which port it used.
The run ended with MAX_CONSECUTIVE_FAILURES_EXCEEDED but every node looks fine.
Something is returning failure or timeout repeatedly with nothing succeeding in between. Look for an action node (not a detection node) that keeps failing.
A node with a retry policy never retried.
Either it returned fatal (never retried), or it returned status success with a failure route (detection "not found"), which is not a retryable result.
The flow takes a different branch than I expect. Check the node's route, not its status. For detection nodes the two differ by design.
FAQ
Does fatal roll anything back?
No. fatal finalizes the run. Any input already sent to the device has already happened.
Can two nodes run at the same time in one flow? No. One session executes one node at a time. Use multiple instances for concurrency.
If a node retries 3 times, do its output mappings run 3 times? No — exactly once, applied to the final attempt's result.
Does a cancelled edge ever get followed?
No. Cancelled ports may exist on the canvas for forward compatibility, but the runtime finalizes on cancelled without following an edge.
Related pages
- Flow Anatomy — what a flow is made of
- Retry and Failure Handling — the retry policy in detail
- Variables and Scope — the execution context this loop carries
- Node Reference — per-node ports and statuses