Error handling
Failures are part of the graph, not an afterthought. Flowdrome gives you layered tools, from node-local to run-global.
The result ledger — every outcome rides the wire
Every node’s outcome — succeeded, failed, skipped or canceled, with the error code and
message when there is one — is appended to the envelope’s meta.result ledger as the run
progresses. A failed node still completes an output envelope, so the reason travels in-band
wherever the flow goes next. The Result node reads the ledger
and routes down one of four ports (succeeded / failed / skipped / canceled), with a
Decide by setting: the literal last entry, the last real step (frames and triggers
skipped), or any failed.
Result rules — On error / On cancelled / On skipped
Every node’s properties carry three result rules, each
Stop (default) · Continue (with the real status) · Continue (with success status):
- On error — Continue lets a failing node keep the run alive: its failure envelope
(reason on the ledger) flows out every wired output port, and downstream nodes — a Result
node above all — can see what happened and route on it. The node still shows
failedin the run detail; the run itself stays green, the same rule a Try catch follows (“a caught failure is not a run failure”). Continue as success stamps the ledger entrysucceededwhile keeping the error recorded. The rule wins over an enclosing Try — it sits on the node itself. - On skipped — Continue makes a dead branch observable: instead of skipping silently, the
node emits a null-payload envelope whose ledger entry says
skipped, so a Result node after it can react to “this path never ran”. - On cancelled only affects the ledger stamp — pressing Stop always still ends the run.
A node with any rule set shows a small shield badge on its card, so changed behavior is never invisible.
Retry without new machinery: wire the failing node (On error: Continue) into a Result
node, its failed port back into a Junction’s Return, and
the junction’s Limit port to your give-up path — a bounded retry loop from ordinary parts.
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: nobody hangs through the backoff. On the default
async webhook the caller already holds its 202 { runId, status, statusUrl } receipt, and the
replays heal the state behind it (“it retried itself at 3am”). Note that the status URL follows the
run it names — the first attempt — so it reports that attempt’s failure even after a later replay
succeeds; each replay is its own run in the ledger, tagged autoreplay#n. A workflow with an
HTTP Response node answers synchronously, so its caller gets
the failed reply immediately and the replays run behind it.
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.