Retry and Failure Handling
Automation on a live screen fails constantly and normally: an animation is still playing, a network call is slow, a dialog appears late. A flow that treats every hiccup as a crash is useless. A flow that retries everything forever is worse.
This page explains the three tools for handling failure, and — more importantly — when each one is the wrong choice.
Purpose
By the end of this page you should be able to look at a failing step and say which of these it needs:
- A longer timeout — the thing is coming, just later.
- A failure edge — the screen is in a different state, take another path.
- A retry policy — the operation genuinely failed and repeating it might work.
Choosing wrongly is why flows become slow, flaky, or both.
Retry is a node property, not a node
There is no "Retry" node. Retry is a policy attached to a node, configured in the Properties panel.
{
"maxAttempts": 3,
"retryOn": ["failure", "timeout"],
"backoffMs": 500,
"backoffMultiplier": 1.5,
"captureScreenshotPerAttempt": true
}
Every setting
| Setting | Type | Range | Default | Meaning |
|---|---|---|---|---|
maxAttempts | integer | 1–20 | — | Total attempts including the first |
retryOn | list | failure, timeout | — | Which results are retryable |
backoffMs | integer | 0–60000 | — | Wait before the next attempt |
backoffMultiplier | number | 1–10 | — | Multiplies the wait each attempt |
captureScreenshotPerAttempt | boolean | — | false | Save a screenshot for each attempt |
maxAttempts: 3 means three attempts total, not one plus three.
With backoffMs: 500 and backoffMultiplier: 1.5, the waits are 500 ms, then 750 ms, then 1125 ms.
What is never retried
retryOn may contain only failure and timeout. The schema rejects anything else, and for good reason:
fatalis never retried. It means a configuration or system error — a malformed expression, an invalid region, a disconnected device. Repeating it produces the identical error.successis never retried, including a detection that reported "not found" (which carries statussuccess).cancelledis never retried. The user stopped the run.
Adding a retry policy to a Find Image to "look harder" does nothing.
A Find Image that does not find its template returns status success with route failure. Retry only fires on status failure or timeout. The policy never triggers.
To look harder, raise timeoutMs — the node already re-captures and re-matches on every poll interval. That is the retry loop for detection.
The attempt lifecycle
Two rules follow from this diagram:
- Parameter errors happen before retry. A bad
{{variable}}isfataland is never retried. - Output mappings run exactly once, on the final attempt's result. A node that retries three times writes its variables once.
Timeouts and backoff
timeoutMson a node is the timeout for each attempt, not for the node overall. A node with a 5-second timeout and 3 attempts can take 15 seconds of attempts plus backoff.- Backoff time is not part of an attempt's timeout.
- Backoff time does count toward the flow's
maxRuntimeDurationMs.
The three tools, and when each is right
1. Raise the timeout
Use when the thing you are waiting for is going to appear, you just don't know exactly when.
Detection nodes poll: they capture a frame, match, sleep for the poll interval, and repeat until the timeout. A longer timeout is more attempts.
The Claim button appears after a 2-second animation. →
Find Image,timeoutMs: 4000,pollIntervalMs: 250.
2. Wire a failure edge
Use when "not there" means the screen is in a different, legitimate state and you want to do something else.
Sometimes there is a daily-reward popup, sometimes there isn't. →
Find Imagepopup, Success → close it, Failure → carry on.
This is branching, not error handling. It is the most common and most under-used of the three.
3. Attach a retry policy
Use when an operation genuinely failed in a way that repeating might fix: input rejected, frame capture hiccup, a transient device error.
The tap is occasionally swallowed while the UI is still settling. →
Click,maxAttempts: 3,retryOn: [failure, timeout],backoffMs: 400.
Decision table
| Situation | Tool | Why |
|---|---|---|
| Button appears after an animation | Longer timeout | It is coming |
| Popup may or may not be present | Failure edge | Two valid states |
| Screen sometimes loads slowly | Longer timeout | Same state, later |
| Tap occasionally not registered | Retry policy | Operation failed |
| Expression references a missing variable | Neither | fatal — fix the config |
| OCR sometimes misreads | Verification loop | See below |
The verification pattern
For anything that matters, do not assume an action worked — check.
This is stronger than retry because it confirms the outcome, not the call. The reference automation studied for this documentation uses exactly this shape: act, settle, verify, and route to a clean skip when verification fails.
Note the Wait between the action and the verification. Without it the verification reads the pre-action frame. See frameSync in Flow Anatomy.
Guardrails interact with failure
The runtime tracks consecutive failures across the whole run:
failureandtimeoutincrement the counter.successresets it to0.- Exceeding
maxConsecutiveFailuresfinalizes the run asfatal.
Because a detection "not found" has status success, a polling loop that finds nothing for a long time does not trip this guardrail. Only genuine operational failures do.
Designing a clean failure path
Give every flow an explicit, intentional ending for the bad case. Two terminal nodes is usually enough:
Put a message on the failure Stop. That message is what you will read in the run log at 2 a.m., and "Max Slot Troop" is infinitely more useful than a node id.
When the flow is a Sub Flow, those two Stops become its return value — the parent branches on the child's Success and Failure ports.
Best practices
- Set a message on every failure Stop. It is your log.
- Prefer verification over retry for anything with a side effect.
- Keep
maxAttemptssmall — 2 or 3. If 3 attempts don't fix it, 10 won't either. - Use backoff for anything device-related. An immediate retry usually hits the same busy state.
- Never retry a
fatal. You cannot, and wanting to means the configuration is wrong. - Wire every Failure port, even if only to a Stop. An unwired route ends the run with
TRANSITION_NOT_FOUND. - Enable
captureScreenshotPerAttemptwhile debugging, then turn it off — it writes an image per attempt.
Performance notes
- Retry multiplies worst-case node time by
maxAttemptsand adds the backoff series. A 5-second timeout with 3 attempts and 500 ms/1.5× backoff is up to ~16 seconds for one node. - Prefer a longer single timeout over multiple retried attempts for detection: polling inside one attempt has no backoff overhead and no repeated setup.
captureScreenshotPerAttemptwrites an image per attempt — real I/O. Leave it off in production flows.
Memory notes
- Every attempt is logged, and with
captureScreenshotPerAttemptevery attempt stores an artifact. Long runs with aggressive retry produce large run folders. - Only the final attempt's result is recorded in
nodeResults, so retry does not multiply the context's memory footprint.
Common mistakes
| Mistake | What actually happens | Fix |
|---|---|---|
| Retry on Find Image to "search harder" | Policy never fires | Raise timeoutMs |
maxAttempts: 1 expecting one retry | That is no retry | Use 2 for one retry |
Retrying a fatal | Not possible | Fix the configuration |
| No wait between action and verification | Verifies a stale frame | Insert a Wait |
| Unwired Failure ports | Run ends TRANSITION_NOT_FOUND | Wire to a Stop at minimum |
| Failure Stop with no message | Unreadable logs | Write the reason |
Huge maxAttempts on a slow node | Run blows its duration cap | Small attempts, real branching |
Troubleshooting
My retry policy never runs.
The node is returning status success (detection not-found) or fatal. Only status failure and timeout are retryable.
The run dies with MAX_CONSECUTIVE_FAILURES_EXCEEDED.
Something is failing repeatedly with no success in between. Detection not-founds don't cause this — look for action nodes.
A node takes far longer than its timeout.
Timeout is per attempt. Multiply by maxAttempts and add backoff.
Variables written by a retried node look wrong. They come from the final attempt only. That is by design.
The flow fails immediately with PARAM_EXPRESSION_INVALID.
A {{variable}} in a parameter could not be resolved — usually undeclared or misspelled. This is fatal and happens before any retry.
FAQ
Can I retry a whole group of nodes? Not directly. Put them in a Sub Flow and branch on its Failure port, or build an explicit loop with a counter.
Does backoff count toward the node's timeout?
No — but it does count toward the flow's maxRuntimeDurationMs.
Is maxAttempts: 3 three retries or three attempts?
Three attempts total: the first plus two retries.
Should I put retry on everything? No. Most flakiness is better solved with a longer timeout or a real branch.
Related pages
- Execution Model — statuses, routes and guardrails
- Flow Anatomy — flow-level defaults and frame sync
- Variables and Memory — what a retried node writes
- Node Reference — per-node failure modes