# Error handling and retries

> What happens when an APIANT automation step fails: the automatic retry schedule, which errors are retried, continue-on-error modes, the error output, error-handler subroutines, and the 401 turn-off.

When a step fails, APIANT first retries it if the error looks temporary, then hands the error to an error-handler subroutine if the automation has one, and otherwise stops the run and reports the error. Each step can instead be set to continue past a failure and pass the error on as data.

## What happens when a step fails

With the default setting, a failed step goes through these stages in order:

1. **Retry.** If the error is retryable, the run is parked and the step is tried again on a fixed schedule.
2. **Step error handler.** If the step has an error-handler subroutine, it runs with the error details.
3. **Automation error handler.** If the step has no handler, or its handler also fails, the automation's error-handler subroutine runs, if there is one.
4. **Stop.** Otherwise the step is marked as failed, the run ends with an error, and an alert is raised.

A step that exceeds the run's time limit fails without retries.

## Automatic retries

A retryable failure is retried on this schedule, which is the same for every automation and cannot be changed:

| Attempt | Waits before it |
|---|---|
| 1 | Runs immediately |
| 2 | 1 minute |
| 3 | 2 minutes |
| 4 | 5 minutes |
| 5 | 10 minutes |
| 6 | 30 minutes |
| 7 | 1 hour |
| 8 | 2 hours |
| 9 (last) | 4 hours |

That is 9 attempts over about 7 hours 48 minutes. While a retry is pending, the run's status is **snoozed** and no alert is sent. If the last attempt fails, the error goes to the error handlers, or the run fails.

- A retry runs the failed step again. Steps that already succeeded in that run are not repeated.
- A failure inside an ordinary automation called synchronously resumes that child run: its steps that already succeeded replay their recorded output instead of running again.
- A polling automation does not park for retries. A record that fails is not marked as processed, so the next poll picks it up again while the app still returns it.
- Turning an automation off removes its pending retries.

### Which errors are retried

| Retried | Not retried |
|---|---|
| HTTP 429, 500, 502, 503 and 504 | HTTP 501 and 505, and 4xx responses other than 429 |
| Request and connection timeouts | A domain name that does not exist |
| Connection refused, reset or dropped mid-response | A request blocked by the platform's network policy |
| Temporary DNS failures and unreachable hosts | Validation errors, such as a value that cannot be converted to a field's type |
| Lock contention inside the platform | |

APIANT also keeps a platform-wide list of error-message fragments that are treated as retryable, so an error in the right-hand column, other than a request blocked by the network policy, can still be retried when its message matches that list. An app's connector can also define its own handling for specific responses, which takes precedence over this table. You cannot change the list, or the retry schedule, from your account. If an app returns an error that is clearly temporary and is not being retried, contact APIANT support with the error text and examples of runs that succeeded on a later attempt.

To retry failed runs by hand, see [Retries, replays and restarts](/docs/runs/retries-and-replays).

## Continuing past a failure

A step's error handling can be set to one of three modes. Ask for it in plain English, for example "if the lookup finds nothing, carry on".

| Mode | On failure |
|---|---|
| Halt (default) | Retry, then error handlers, then stop the run |
| Continue on non-critical error (`continueOnNoncriticalError`) | Continue, unless the failure is an HTTP 5xx or 404, in which case it halts |
| Continue on any error (`continueOnAnyError`) | Continue, whatever the failure |

A step that continues is not retried and does not run an error handler. It is recorded as **recovered**, counts no task, and its output carries the error so later steps can act on it.

Use Halt for steps that must succeed, such as creating a record. Use a continue mode for lookups that may legitimately find nothing and for checks such as "does this record still exist?".

## The error output

Every step output has an `error` field. It is `null` when the step succeeded. When a step failed and the run continued, it holds:

| Field | Contains |
|---|---|
| `error.message` | The error text |
| `error.status` | The HTTP status for an app error, such as `404` or `429`; `0` otherwise |
| `error.vendor_payload` | The app's response body, decoded if it is JSON; `null` if there was none |
| `error.vendor_headers` | The app's response headers |
| `error.retryable` | Whether this kind of error is retryable. It does not mean another retry will happen. |

A step that continued also outputs `continued_on_error: true`.

Follow a continue-mode step with a condition that checks for success first, then routes on the status:

```json
{
  "step_id": "lookup_succeeded",
  "type": "condition",
  "if": "steps.lookup_client.output.error == nil",
  "then": [ { "step_id": "update_client", "type": "assembly" } ],
  "else": [
    {
      "step_id": "was_rate_limited",
      "type": "condition",
      "if": "steps.lookup_client.output.error != nil && steps.lookup_client.output.error.status == 429",
      "then": [ { "step_id": "notify_rate_limit", "type": "assembly" } ]
    }
  ]
}
```

Always check that `error` is not `null` before reading `error.status`. A test that reads `error.status` on a step that succeeded evaluates as false and takes the `else` branch without failing the run.

Route on `error.status`, not on the wording of `error.message`. Apps change their messages.

## Error-handler subroutines

An error handler is a subroutine that runs when a step fails after its retries. It can be attached to one step, to the whole automation, or both.

The handler receives values you map from `{{ error }}`:

```json
"on_error": {
  "subroutine_uuid": "<error handler subroutine id>",
  "input_mapping": {
    "failed_step": "{{ error.step_id }}",
    "status": "{{ error.status }}",
    "message": "{{ error.message }}",
    "detail": "{{ error.vendor_payload }}"
  }
}
```

Inside the handler, these arrive as `{{ trigger.input.failed_step }}`, `{{ trigger.input.status }}` and so on. In the input mapping, `error` is never `null`, and `error.step_id` names the step that failed.

When the handler finishes, the failed step is marked **recovered** and the run continues with the next step. The step's output then holds `error`, `on_error_recovered: true`, the handler's `subroutine_uuid`, and the handler's step outputs under `outputs`. An automation-level handler reports `doc_on_error_recovered` instead.

Typical handlers post the failure to a team channel, write the record to a review table, or create a ticket. See [Subroutines and calling automations](/docs/automations/subroutines).

## When an app returns 401

A 401 response usually means a connection's credentials were revoked or expired. When a run ends with an error because a step got a 401, APIANT turns the automation off and queues the notice "A 401 error turned off this automation; reconnect the account." Runs stop until you reconnect and turn the automation back on.

- A 401 on a step set to continue past errors, or one recovered by an error handler, does not turn the automation off.
- If refreshing the connection's token also fails, or the app returns a 403 saying the token lacks a required permission, the connection is marked as needing to be reconnected. Runs keep failing until you reconnect.
- Some apps return 401 for rate limits or quotas. APIANT keeps a platform-wide list of such domains, where a 401 is handled as an ordinary error instead. The list is managed by APIANT; contact support with evidence if an app belongs on it.

See [Connecting accounts](/docs/apps/connections).

## Alerts and turning off on any error

When a run ends with an error, the alert goes through the automation's alert settings, found in the dashboard gear menu under **Processing > Alert handling**:

| Setting | Default | Effect |
|---|---|---|
| Email me on any alert | On | Queues an alert email for this automation's errors |
| Turn off automation on any alert | Off | Turns the automation off at its first error, not only on a 401 |
| Alert mappings… | None | Rewrites or suppresses alert messages that match a pattern |

Queued alert emails, including the 401 notice, are delivered according to **Alert email delivery** in the account menu's **Settings…**: Immediately, Hourly, Daily or Never. The default is **Never**, and with Never, queued alerts are discarded rather than sent. Set it to Immediately, Hourly or Daily to receive error and 401 emails.

See [Error alerts](/docs/runs/alerts) for alert mappings, suppressed alerts and delivery.

## Stopping a record on purpose

To fail a record deliberately, for example when required data is missing, use the Flow Control action **Halt data row processing with error** with your own message. The record ends with that error and follows the alert settings above. To skip a record without an error, use **Halt data row processing**. See [Conditions, loops and parallel branches](/docs/automations/control-flow#stopping-a-record).

## Troubleshooting

| Symptom | Likely cause | What to do |
|---|---|---|
| An automation turned itself off | A 401, or **Turn off automation on any alert** is on | Reconnect the app if it was a 401, then turn the automation on |
| A temporary error was not retried | The step uses a continue mode, the error class is not retryable, or the automation polls | Check the step's error handling mode and the error's status |
| No error emails arrive | **Alert email delivery** is set to Never | Change it under **Settings…** in the account menu |
| A run shows green but data is missing | A continue-mode step failed and was recovered | Look for recovered steps in the run and check their `error` output |
| A condition on `error.status` always takes `else` | The test reads `error.status` without checking `error != nil` first | Add the `error != nil &&` guard |

## Next steps

- [Retries, replays and restarts](/docs/runs/retries-and-replays)
- [Error alerts](/docs/runs/alerts)
- [Subroutines and calling automations](/docs/automations/subroutines)
- [Troubleshooting automations](/docs/runs/troubleshooting)
