Error handling
Failures are part of the graph, not an afterthought. Flowdrome gives you three layers, from node-local to run-global.
Try frames — catch around a section
Try is a frame: drag nodes into it and it becomes an error boundary. Members execute normally; when one throws, it is retried per the frame’s policy, and if it still fails the exception routes out of the frame’s error port — with the error details as data — instead of failing the run. Success flows out of the success port as usual.
Use it like you’d use try/catch: around the flaky HTTP call, the parse that sometimes meets
garbage, the third-party API with moods.
Retries + delay. The frame’s policy is two numbers: Retries (how many times to re-run a
failed member before giving up, default 2) and Retry delay (how long to wait between attempts,
in milliseconds, default 0 — instant). A member that fails is re-run up to Retries times, pausing
Retry delay between each attempt; only then does it fall through to the error port. The retries
are visible after a run: the failed step shows ↻ ran N× (with the wait) in the run history and
its detail, so you can see it actually retried. Inside the retried node, the $attempt expression
reads the current try (1 on the first run, then 2, 3, …) — so a node can back off, vary a jitter,
or branch on how many times it’s been tried.
finally — cleanup that always runs, last. A Try has a third output, finally, that follows
the try/catch/finally contract: whatever happens, the finally branch runs after the
success/error branch has finished — and it runs even if that branch itself fails (e.g. an error
leg that ends in Stop and Error). Wire cleanup there (close a handle, release a lock, post a “done”
notice). It only runs when something is wired to it. It carries the outcome envelope but is not the
run’s result — it’s cleanup, so it never overwrites the output the success/error branch produced.
The error envelope carries the failure as data on the error port:
exception.message, exception.code, exception.node, exception.count (how many members failed),
exception.errors[] (each with its own attempts), and the retry accounting —
exception.attempts (how many times the first failed node ran), exception.maxRetries and
exception.maxAttempts (the frame’s budget). So a recovery branch can branch on how hard it tried.
The recovery lane — Error Trigger
The Error Trigger starts a recovery phase for the workflow: when a run fails outside any Try frame, the engine executes the error lane rooted at the Error Trigger, handing it the failure context (failed node, error, the run’s data). The main lane and the recovery lane live in the same document — one workflow, its happy path and its incident response.
Typical recovery lanes: notify Slack/Telegram with the error and run id, write the failure to a queue or database, call a fallback API.
Failure-alert presets — one import away
You don’t have to build the pager lane by hand: the template gallery ships ready-made recovery lanes — Slack alert on failure, email on failure, and the console pager — import one, point the send node at your credential, and every unhandled failure pages you. They compose with autoreplay: retry first, page when the retries are exhausted.
Stop and Error — fail on purpose
Stop and Error fails the run immediately with your message and data. Combine it with If/Check to turn business rule violations into first-class failures that Try frames and the recovery lane see — instead of letting bad data limp downstream.
Per-item error policy
When a node runs once per item, its error policy decides what a poison item does: fail the run, or drop the item and count it (surfaced in the node’s run meta). One bad row out of 100,000 doesn’t have to cost you the other 99,999.
After the fact: retry from a node
Every failed run records its envelopes, so the fix-and-retry loop doesn’t start from the trigger: retry from the failing node re-executes only the downstream subgraph, seeded with the recorded inputs. Fix the config, retry from the node, watch it go green.
On deployed hosts
All of the above deploys as-is: Try frames, the recovery lane, stop-and-error and per-item
policies behave identically in the editor and on a host — it is the same engine executing the
same document. An approval gate’s timeout policy (approve / reject / error on expiry)
also feeds the same machinery — an expired gate can deliberately fail the run into its
recovery lane.
Autoreplay — deployed runs that retry themselves
A deployed workflow can carry a replay policy in its document:
"meta": { "autoreplay": { "maxAttempts": 2, "backoffSeconds": 30 } }
When a served run fails — a webhook call, a schedule tick, a bot message — the host
re-runs it with the same input, waiting backoffSeconds × 2^(n−1) before each attempt
(so 30s, then 60s, …), up to maxAttempts (1–10). Each attempt is an ordinary run in the
host’s ledger, tagged autoreplay#n, so the Runs view shows exactly what happened and when.
Replays stop early on the first success, and never fire for a paused or stopped workflow.
Autoreplay is deliberately fire-and-forget: an HTTP caller gets the failed reply immediately rather than hanging through backoff — the replays heal the state behind it (“it retried itself at 3am”). For request/response APIs where the caller needs the retry, put a Try frame with a retry policy around the flaky section instead — the two compose: Try retries inside a run, autoreplay retries the run.