Skip to main content

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 Image with 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.
Where real parallelism comes from

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:

  1. fatal and cancelled never follow an edge. Wiring a Fatal port does not create a recovery path during a normal run — the runtime finalizes the moment it sees that status. Use failure for anything you intend to recover from.
  2. A route with no edge ends the run. If a node returns failure and nothing is wired to its Failure port, the run finalizes as failure with TRANSITION_NOT_FOUND. It does not silently continue.

Status versus route — the distinction that matters most

Every node returns two separate things:

What it describesWho reads it
statusExecution health — did the node itself work?The runtime, for guardrails and retry
routeWhich port the flow leaves throughEdge resolution, to pick the next node

They are not the same value, and for detection nodes they deliberately disagree.

The five statuses

StatusMeaningRetryableFollows an edge
successNode completed normallyYes
failureRecoverable failureYesYes
timeoutExpected condition not met in timeYesYes
cancelledRun was stopped mid-nodeNoNo — finalizes
fatalUnrecoverable system or configuration errorNoNo — 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.

Read it as a question, not a verdict

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):

  1. Candidates are pre-indexed at compile time by (fromNodeId, fromPort).
  2. They are sorted by priority ascending, then by edge id ascending.
  3. The first candidate with no condition wins.
  4. A candidate with a condition is evaluated; true selects it, false moves on.
  5. If a condition errors or returns a non-boolean, that candidate is skipped and the run continues to the next one.
  6. 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 returns fatal with PARAM_EXPRESSION_INVALID before 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.

GuardrailCheckedEffect when exceeded
maxNodeExecutionsBefore each nodefinalize fatal, MAX_NODE_EXECUTIONS_EXCEEDED
maxRuntimeDurationMsBefore each nodefinalize fatal, MAX_RUNTIME_DURATION_EXCEEDED
maxConsecutiveFailuresAfter each nodefinalize fatal, MAX_CONSECUTIVE_FAILURES_EXCEEDED

The consecutive-failure counter behaves like this:

  • failure and timeout increment it.
  • success resets it to 0.
  • fatal and cancelled finalize the run before the counter is consulted.

Because a detection "not found" carries status success, it resets the counter rather than incrementing it.

Long-running flows need headroom

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:

CauseFinal status
Reached a Stop nodewhatever the Stop declares (success / failure / cancelled)
Reached a Return nodewhatever the Return declares
A node returned fatalfatal
A node returned cancelled, or the user stopped the runcancelled
A route had no matching edgefailure (TRANSITION_NOT_FOUND)
A guardrail was exceededfatal

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:

StepNodestatusrouteNext
1StartsuccesssuccessFind Image
2Find ImagesuccessfailureWait 2s
3Wait 2ssuccesssuccessFind Image (2nd)
4Find Image (2nd)successsuccessClick
5ClicksuccesssuccessStop
6Stopfinalize 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 failure for recovery, not fatal. fatal finalizes 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 success reset your failure counter. Interleaving successful steps between risky ones naturally keeps maxConsecutiveFailures from 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 timeoutMs covers 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, and lastResult. Running the same flow on 10 instances creates 10 independent contexts.
  • nodeResults grows with every node executed, because each node's result is recorded for later reference (this is what lets an If check an upstream node's outcome). A flow with a very high maxNodeExecutions and 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

MistakeWhat happensFix
Leaving a Failure port unwiredRun ends with TRANSITION_NOT_FOUNDWire it, even if only to a Stop
Expecting Fatal to act as a catch-allNever followed; the run finalizesHandle recoverable problems on failure
Adding retry to a Find Image to "look harder"Retry does not fire — not-found has status successRaise timeoutMs, or branch on Failure
Treating "not found" as an error in logsMisreads a healthy flow as brokenCheck the route, not just the status
Very long flow with default maxNodeExecutionsDies mid-run with MAX_NODE_EXECUTIONS_EXCEEDEDRaise 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.