Skip to main content

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:

  1. A longer timeout — the thing is coming, just later.
  2. A failure edge — the screen is in a different state, take another path.
  3. 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

SettingTypeRangeDefaultMeaning
maxAttemptsinteger1–20Total attempts including the first
retryOnlistfailure, timeoutWhich results are retryable
backoffMsinteger0–60000Wait before the next attempt
backoffMultipliernumber1–10Multiplies the wait each attempt
captureScreenshotPerAttemptbooleanfalseSave 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:

  • fatal is never retried. It means a configuration or system error — a malformed expression, an invalid region, a disconnected device. Repeating it produces the identical error.
  • success is never retried, including a detection that reported "not found" (which carries status success).
  • cancelled is never retried. The user stopped the run.
The most common retry mistake

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:

  1. Parameter errors happen before retry. A bad {{variable}} is fatal and is never retried.
  2. Output mappings run exactly once, on the final attempt's result. A node that retries three times writes its variables once.

Timeouts and backoff

  • timeoutMs on 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 Image popup, 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

SituationToolWhy
Button appears after an animationLonger timeoutIt is coming
Popup may or may not be presentFailure edgeTwo valid states
Screen sometimes loads slowlyLonger timeoutSame state, later
Tap occasionally not registeredRetry policyOperation failed
Expression references a missing variableNeitherfatal — fix the config
OCR sometimes misreadsVerification loopSee 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:

  • failure and timeout increment the counter.
  • success resets it to 0.
  • Exceeding maxConsecutiveFailures finalizes the run as fatal.

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 maxAttempts small — 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 captureScreenshotPerAttempt while debugging, then turn it off — it writes an image per attempt.

Performance notes

  • Retry multiplies worst-case node time by maxAttempts and 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.
  • captureScreenshotPerAttempt writes an image per attempt — real I/O. Leave it off in production flows.

Memory notes

  • Every attempt is logged, and with captureScreenshotPerAttempt every 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

MistakeWhat actually happensFix
Retry on Find Image to "search harder"Policy never firesRaise timeoutMs
maxAttempts: 1 expecting one retryThat is no retryUse 2 for one retry
Retrying a fatalNot possibleFix the configuration
No wait between action and verificationVerifies a stale frameInsert a Wait
Unwired Failure portsRun ends TRANSITION_NOT_FOUNDWire to a Stop at minimum
Failure Stop with no messageUnreadable logsWrite the reason
Huge maxAttempts on a slow nodeRun blows its duration capSmall 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.