Skip to main content

Error handling

By default a failed step ends the run. This page covers the three alternatives you can choose per step: retry, continue past the failure, or route the failure to a fallback step.

All three live under error_handling on the step.

Retry a failed step

Use a retry policy first whenever a step talks to something over a network:

- id: create_item
type: module
plugin_type: np-action-item-create
error_handling:
retry_policy:
max_attempts: 3
initial_interval: "2s"
backoff_strategy: exponential
jitter: 0.3
config:
apiKey: "${{ secrets.NP_API_KEY }}"

Always set jitter on a step that runs in parallel. Without it, children that all fail against the same rate-limited API retry at the same moment and collide again on every attempt.

Retries only apply to failures the plugin marks as retryable. A bad NRN, a malformed payload, or a payload that exceeds a size limit is non-retryable: the same call would fail the same way, so the engine doesn't spend attempts on it.

A retry policy turns off automatic fan-out

max_attempts above 1 on a step that would otherwise dispatch once per item makes it a single whole-batch call instead. If you need both, move the work into a sub-workflow and put the retry on the steps inside it.

Continue the run after a failure

Set continueOnError to let the run keep going when the step fails. The step is recorded as failed, and the next steps run:

- id: close_items
type: module
plugin_type: np-action-item-update
forEach:
expression: "${{ steps.resolved.items }}"
itemVariable: item
error_handling:
continueOnError: true

Anything that reads that step's output has to handle it being absent. The usual pattern is a decider right after it that reads steps.close_items.status and routes the success and failure cases separately.

On a forEach step this is the only option. The engine only checks continueOnError for iterations, so a fallback_step declared on the same step is silently ignored. That's how the engine works, not a configuration mistake, and it's why the published suites pair continueOnError with a decider instead of a fallback step whenever the failing step iterates.

Route a failure to a fallback step

Set fallback_step to the step that should run when this one fails:

- id: wait_for_approval
type: module
plugin_type: signal-wait
error_handling:
fallback_step: resolve_timeout

There is one requirement that is easy to miss. At runtime the engine reaches the fallback through a failure edge, but the graph is validated when you save it, and a step with no incoming connection looks like an entry point or dead code. So you also have to declare a connection that is never taken:

connections:
# Permanently false: the success path never takes it. The engine reaches
# resolve_timeout through error_handling.fallback_step instead.
- { from: wait_for_approval, to: resolve_timeout, condition: "false" }

Without that line the fallback either never fires or the workflow fails validation outright.

Give each failure path its own final node

Don't point the success path and the failure paths at one shared final node. It does not work, and it fails as a hang rather than as an error.

A node fed by one completed edge and one failure edge can't start under any join strategy. any only fires on a predecessor that completed, so it ignores the failure edge and waits forever for a success that never comes. all waits on the edge that was neither settled nor skipped, and a condition: "false" edge is not reliably marked skipped.

Give every path its own final node with a single predecessor. With one incoming edge, the default all join is satisfied as soon as that predecessor finishes:

- id: resolve_ok        # single completed edge from the success path
- id: resolve_timeout # single failure edge via wait_for_approval's fallback_step
- id: resolve_failed # single failure edge via create_issue's fallback_step

If you need the paths to meet, connect those final nodes to a common step afterwards, once every path is a normal completed edge.

React to a failed run from another workflow

Two triggers let another workflow respond when one fails.

Use on-error to attach alerting or compensation to one specific workflow: it fires when a step or the workflow in the same definition fails. Use execution-failed-trigger to build one recovery or notification workflow for everything: it fires when any execution in your organization ends in failure, optionally narrowed to one workflow.

Both are ordinary triggers: they register on activation and nothing fires until then.

Find the cause of a failed run

Start with the execution error. It names the step, the plugin's error code, and the underlying message, and the canvas marks the step that failed.

Three cases need a different reading:

  • A step that failed before running. Config validation happens before execution, so a validation error won't appear in that step's logs. Look at the execution error instead.
  • A size limit. STEP_OUTPUT_TOO_LARGE, STEP_INPUT_TOO_LARGE, EXECUTION_STATE_TOO_LARGE and HISTORY_LIMIT_EXCEEDED are covered in limits, and none of them retry.
  • A run ended by the platform. On a very large fan-out the platform can end the run even though no step failed. The error says so explicitly and notes that completed steps kept their results, which is why the canvas stays green in that case.

Next steps