# fallback (/docs/macros/fallback)



A native try/catch. Attach `fallback:` to a step (or a job) to **recover or notify**
when it fails. To simply *tolerate* a failure and continue, see
[`soft_fail`](/docs/macros/soft-fail); for teardown that runs regardless of outcome,
see [`lifecycle`](/docs/macros/lifecycle).

## Options [#options]

`fallback:` is either a **list of steps** (the catch body, shown below) or an object
with these keys:

<TypeTable
  type="{
  recover: {
    description: 'Swallow the failure (continue-on-error) instead of just notifying. See &#x22;recover: true&#x22;.',
    type: 'boolean',
    default: 'false',
  },
  retry: {
    description: 'Re-run the same step once in a sibling job on a different runner. See &#x22;retry: different runner&#x22;.',
    type: 'boolean | object',
  },
  'retry.runs-on': {
    description: 'The runner for the retry job (e.g. a bigger box).',
    type: 'string',
  },
  'retry.when-exit-code': {
    description: 'Only retry on these exit codes (e.g. 137 = OOM). Number or list.',
    type: 'number | number[]',
  },
}"
/>

## Default = notify [#default--notify]

The error is *not* swallowed; fallback runs via `if: failure()`, the job still fails.

The guarded step gets an `id`; the fallback is gated on its `conclusion`:

<CodeCompare>
  ```yaml title=".actio.yml"
  jobs:
    deploy:
      runs-on: ubuntu-latest
      steps:
        - name: Deploy
          run: ./deploy.sh
          fallback:
            - name: Notify
              run: ./notify.sh failure
  ```

  ```yaml title="generated .yml"
  steps:
    - name: Deploy
      run: ./deploy.sh
      id: step_deploy # [!code highlight]
    - name: Notify
      run: ./notify.sh failure
      if: failure() && steps.step_deploy.conclusion == 'failure' # [!code highlight]
  ```
</CodeCompare>

## recover: true [#recover-true]

`recover: true` = true try/catch: the guarded step gets `continue-on-error: true`
and the fallback uses `steps.<id>.outcome == 'failure'`, so the job can continue.

<Callout title="outcome vs conclusion">
  With `continue-on-error`, a failed step's `conclusion` becomes `success`, so
  recovery must key off `outcome`. Actio gets this right for you.
</Callout>

## retry: different runner [#retry-different-runner]

Some failures aren't your code — they're the runner (OOM kills, flaky hosted
infra, a job that needs more cores). `retry:` re-runs the *same step* in a brand
new job on a different `runs-on`, gated on the original job failing.

<Callout title="fallback.retry vs the retry macro">
  Don't confuse this with the standalone [`retry`](/docs/macros/retry) macro, which
  re-runs the **same step on the same runner** N times for transient flakiness.
  `fallback.retry` runs the step **once on a different runner**, for infra-level
  failures.
</Callout>

The guarded step is lifted into a sibling `<job>_retry` job with
`needs: [<job>]` and `if: failure()`, running on the fallback runner:

<CodeCompare>
  ```yaml title=".actio.yml"
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - name: Checkout
          uses: actions/checkout@v4
        - name: Build
          run: ./build.sh
          fallback:
            retry:
              runs-on: ubuntu-latest-8-cores
  ```

  ```yaml title="generated .yml"
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - name: Checkout
          uses: actions/checkout@v4
        - name: Build
          run: ./build.sh
    build_retry: # [!code highlight:9]
      name: Retry "Build" on ubuntu-latest-8-cores
      needs:
        - build
      if: failure()
      runs-on: ubuntu-latest-8-cores
      steps:
        - name: Build
          run: ./build.sh
  ```
</CodeCompare>

### when-exit-code [#when-exit-code]

Only retry on *specific* exit codes (e.g. `137` = SIGKILL/OOM) instead of any
failure. Actio captures the original step's real exit code via an `EXIT` trap,
exposes it as a job output, and gates the retry job on it.

<CodeCompare>
  ```yaml title=".actio.yml"
  - name: Run tests
    run: ./run-tests.sh
    fallback:
      retry:
        runs-on: ubuntu-latest-16-cores
        when-exit-code: [137]
  ```

  ```yaml title="generated .yml"
  jobs:
    test:
      runs-on: ubuntu-latest
      steps:
        - name: Run tests
          run: |-
            trap 'echo "exit_code=$?" >> "$GITHUB_OUTPUT"' EXIT
            ./run-tests.sh
          id: step_run_tests # [!code highlight]
      outputs: # [!code highlight:2]
        step_run_tests_exit_code: ${{ steps.step_run_tests.outputs.exit_code }}
    test_retry: # [!code highlight:9]
      name: Retry "Run tests" on ubuntu-latest-16-cores
      needs:
        - test
      if: failure() && needs.test.outputs.step_run_tests_exit_code == '137'
      runs-on: ubuntu-latest-16-cores
      steps:
        - name: Run tests
          run: ./run-tests.sh
  ```
</CodeCompare>

`when-exit-code` accepts a single number or a list (`[137, 143]`, OR-joined).

<Callout type="warn" title="Limitations (v1)">
  * **`when-exit-code` is `run`-only and POSIX-shell-only.** It relies on a shell
    `EXIT` trap, so it's skipped (with a warning) on `uses:` steps or non-POSIX
    shells; the retry then falls back to a plain `if: failure()` gate.
  * **The guarded step must be self-contained.** Preceding in-job setup steps
    (checkout, tool installs, etc.) are *not* copied into the retry job. Put any
    setup the step needs inside the step, or this mode isn't a fit.
  * **The workflow still goes red.** The retry job is an independent recovery
    lane — a green sibling cannot un-fail the original job. Downstream jobs that
    `needs:` the original are unaffected and will not see the retry's result.
  * **One retry, no loop.** Exactly one `_retry` job is emitted; there's no
    retry-of-the-retry.
  * **Step-level only.** `retry:` on a job-level `fallback` is ignored (warning).
</Callout>


## Sitemap

Browse the full documentation: [Markdown sitemap](https://austenstone.github.io/actio/sitemap.md) · [XML sitemap](https://austenstone.github.io/actio/sitemap.xml)