# Retry failed steps in GitHub Actions at the right level

> GitHub Actions retry failed step options compared: a retry action, a job re-run, runner retries and continue-on-error, and how to keep each visible.

Source: https://latchkey.dev/learn/flaky-tests/retry-failed-steps-in-github-actions  
Updated: 2026-11-12

GitHub Actions has no built-in step retry, so to have GitHub Actions retry failed steps you choose one of four patterns: a retry action such as nick-fields/retry, a job re-run with `gh run rerun --failed`, the test runner's own retries, or `continue-on-error` with a second step. Each repeats something different, so retry at the level that gets a fresh copy of whatever failed, and never retry an assertion silently.

A step failed for a reason that had nothing to do with your change, and you want the workflow to try again by itself. The workflow syntax has `continue-on-error` and `timeout-minutes` but no retry key, so every option is a pattern on top of something else.

The patterns are not interchangeable. A step retry reruns a command on the same machine. A job re-run gets a new machine. A test retry reruns one test inside the same process tree. Choosing the wrong one either fails to fix the failure or hides the bug that caused it.

## Four levels, four different things repeated

Pick by what failed. A network fetch inside a step wants the step retried. A machine that ran out of memory or lost its connection wants a new machine, which only a job re-run provides. A single test that flips wants the test runner, because the runner is the only level that knows which test it was.

| Level | Mechanism | What runs again | New machine | What it can hide |
| --- | --- | --- | --- | --- |
| Step | `nick-fields/retry`, or a second step after `continue-on-error` | One command | No | An assertion failure, if the command is the test suite |
| Job | `gh run rerun --failed`, or Re-run failed jobs in the UI | Failed jobs and their dependents | Yes on GitHub-hosted | Any intermittent bug, unless the attempt count is recorded |
| Test | Playwright `retries`, `jest.retryTimes`, `pytest --reruns`, Surefire `rerunFailingTestsCount` | The failed test | No | Races and shared state, unless flaky results are reported |
| None | `continue-on-error: true` | Nothing; the failure is ignored | No | Everything that step or job checks |

## Step level: retry the fetch, not the suite

nick-fields/retry v4.0.0 is the common step-level option. It needs `command`, `max_attempts`, and exactly one of `timeout_minutes` or `timeout_seconds`. Version 4 moved the action to the Node 24 runtime; the inputs did not change.

Read `retry_on` carefully. In the action's source, `error` means any nonzero exit, so wrapping `npm test` retries assertion failures exactly like network errors. Wrap the step that talks to the network, such as the install, and leave the test step alone. `retry_on_exit_code` narrows it to one exit code.

The action keeps its retries visible by default: `warning_on_retry` is true, so each failed attempt becomes a warning annotation reading `Attempt 1 failed. Reason:` followed by the error, and the `total_attempts` output can go into the step summary.

```.github/workflows/ci.yml
- id: install
  uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
  with:
    command: npm ci
    max_attempts: 3
    timeout_minutes: 10
    retry_on: error
- if: always()
  run: echo "npm ci attempts=${{ steps.install.outputs.total_attempts }}" >> "$GITHUB_STEP_SUMMARY"
- run: npm test   # not wrapped: an assertion fails once and stays failed
```

> Inputs from action.yml and src/index.ts in nick-fields/retry at v4.0.0 (released 2026-03-20), read 2026-11-12.

## Job level: a new machine, the same commit

A job re-run runs the same `GITHUB_SHA` on a new runner, which is the only retry that helps when the machine itself was the problem. From the UI it is Re-run jobs, then Re-run failed jobs; from the terminal it is `gh run rerun <run-id> --failed`, which also re-runs the jobs that depend on them. A run can be re-run at most 50 times, within 30 days.

To make it automatic without making it blind, trigger on `workflow_run`, re-run once, and only when the failed log carries a machine signal. The `run_attempt == 1` guard caps it at one automatic re-run. Note that `workflow_run` triggers only if the workflow file exists on the default branch.

```.github/workflows/rerun.yml
name: Re-run machine failures once
on:
  workflow_run:
    workflows: [CI]
    types: [completed]
permissions:
  actions: write
jobs:
  rerun:
    if: github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1
    runs-on: ubuntu-latest
    env:
      GH_REPO: ${{ github.repository }}
      GH_TOKEN: ${{ github.token }}
      RUN_ID: ${{ github.event.workflow_run.id }}
    steps:
      - run: |
          if gh run view "$RUN_ID" --log-failed |
             grep -qE 'exit code 137|lost communication with the server|No space left on device|ECONNRESET|EAI_AGAIN'; then
            echo "Re-ran run $RUN_ID once: machine signal in the failed log" >> "$GITHUB_STEP_SUMMARY"
            gh run rerun "$RUN_ID" --failed
          else
            echo "Left run $RUN_ID red: no machine signal" >> "$GITHUB_STEP_SUMMARY"
          fi
```

> Re-run limits from the re-running workflows and jobs page, and the `workflow_run` default-branch rule from the events reference, both on docs.github.com, read 2026-11-12. canonical/snapd runs the same shape (`workflow_run`, `actions: write`, `gh run rerun --failed` with `github.token`, a `run_attempt` cap) in .github/workflows/rerun.yaml.

## Test level: the runner's own retries

Test runners retry one test, in the same job, and the good ones record that they did. Playwright marks a test that failed then passed as flaky. Surefire counts it under Flakes in its summary. pytest-rerunfailures prints rerun counts, and its `--only-rerun` takes a regular expression, so you can rerun a connection error and never an `AssertionError`. Jest's `retryTimes` works only with the default jest-circus runner and must be called at the top level of a file or in a `describe` block.

This is the right level for evidence, the wrong level for machine faults: a retried test still runs on the machine that just ran out of memory.

```Test runner retry settings
# Playwright, playwright.config.ts
retries: process.env.CI ? 2 : 0,

# Jest (jest-circus), top of a test file
jest.retryTimes(2, { logErrorsBeforeRetry: true });

# pytest-rerunfailures: rerun connection errors, never assertions
pytest --reruns 2 --only-rerun ConnectionError

# Maven Surefire
mvn -Dsurefire.rerunFailingTestsCount=2 test
```

## continue-on-error is not a retry

`continue-on-error: true` on a step lets the job pass when the step fails; on a job it lets the workflow run pass when the job fails. Nothing runs again. Its legitimate uses are experimental matrix legs and the native retry below, where `steps.<id>.outcome` still reads `failure` while `conclusion` reads `success`, so a second step can try once more and say so with a warning annotation.

```.github/workflows/ci.yml
- id: first
  continue-on-error: true
  run: docker pull ghcr.io/acme/test-db:16
- if: steps.first.outcome == 'failure'
  run: |
    echo "::warning title=Retried::docker pull failed once and was retried"
    docker pull ghcr.io/acme/test-db:16
```

> `continue-on-error`, `outcome` and `conclusion` as defined in the workflow syntax and contexts references on docs.github.com, read 2026-11-12.

## FAQ

### Does GitHub Actions have a built-in retry for steps?

No. The workflow syntax has `continue-on-error` and `timeout-minutes` but no retry key. Use a retry action such as nick-fields/retry for a step, `gh run rerun --failed` for jobs, or your test runner's retries for individual tests.

### How do I re-run only the failed jobs in GitHub Actions?

In the run summary, open the Re-run jobs dropdown and choose Re-run failed jobs, or run `gh run rerun <run-id> --failed`. Jobs that depend on the failed ones run again too. The re-run uses the same commit SHA and the privileges of the actor who triggered the original run.

### How do I retry a GitHub Actions step on failure?

Wrap the command in nick-fields/retry with `max_attempts`, a timeout and `command`, or run the step with `continue-on-error: true` and add a second step that runs when `steps.<id>.outcome` is `failure`. Retry the network call, not the test suite.

### Do retries hide real bugs?

They can. A retry that turns a race into a green build removes the evidence while the bug stays. Retries are safe when they are scoped to machine and network failures and when every retry leaves a record: a warning annotation, a flaky count or an attempt number in the summary.

## References

- [nick-fields/retry: action.yml and README at v4.0.0](https://github.com/nick-fields/retry)
- [GitHub Docs: re-running workflows and jobs (read 2026-11-12)](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/re-run-workflows-and-jobs)
- [GitHub Docs: workflow syntax, continue-on-error](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)
- [pytest-rerunfailures README](https://github.com/pytest-dev/pytest-rerunfailures)
- [Maven Surefire: rerun failing tests](https://maven.apache.org/surefire/maven-surefire-plugin/examples/rerun-failing-tests.html)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
