---
sidebar_label: Error handling
toc_max_heading_level: 3
doc_id: 879516a9-c218-4785-9ee3-ae5e6c1d3aa1
description: >-
  What happens when a nullplatform workflow step fails: retries, continuing
  past a failure, routing to a fallback lane, and the triggers that react to
  a failed run.
keywords:
  - workflows
  - error handling
  - retries
  - fallback
  - continueOnError
  - on-error
  - nullplatform
---

# 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:

```yaml
- 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](/docs/workflows/building-blocks/limits) is non-retryable: the same call would fail the same way, so the engine doesn't spend attempts on it.

:::warning 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:

```yaml
- 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:

```yaml
- 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:

```yaml
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](/docs/workflows/building-blocks/items-and-data-flow#join-several-steps-into-one-with-a-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:

```yaml
- 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](/docs/workflows/building-blocks/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](/docs/workflows/building-blocks/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

- [Items and data flow](/docs/workflows/building-blocks/items-and-data-flow): join strategies, and why failure paths need their own nodes
- [Loops and iteration](/docs/workflows/building-blocks/loops): isolating a failed iteration
- [Runs and versions](/docs/workflows/building-blocks/runs-and-versions): finding the failure in a run
- [Limits](/docs/workflows/building-blocks/limits): the failures caused by size and time limits
