Skip to content
Latchkey

GitHub Actions if: always() Runs a Step Even After Cancellation

Status-check functions in if: do not behave the way they read. always() also runs when a run is canceled, and adding any if: removes the implicit success() gate, so a step you meant to gate now runs in states you did not expect.

What this error means

A cleanup or notification step runs even when the workflow was canceled, or a step runs after a previous failure because adding an if: silently dropped the default "only on success" behavior.

.github/workflows/ci.yml
- run: ./upload-logs.sh
  if: always()        # also runs when the job is CANCELED, not just on failure
# wanted: run on success or failure, but not on cancel

Common causes

always() includes the canceled state

always() means run regardless of status - including cancellation. To run after success or failure but skip cancellation, use !cancelled() instead.

Any if: drops the implicit success() gate

Without an if:, a step only runs if prior steps succeeded. The moment you add any if: (for example if: github.ref == ...), that default gate is gone, so the step can run after a failure unless you re-add success().

How to fix it

Use the function that matches the states you want

Combine status functions explicitly so the step runs in exactly the intended states.

.github/workflows/ci.yml
# run on success or failure, but NOT on cancel
- run: ./notify.sh
  if: ${{ !cancelled() }}
# run only when something failed
- run: ./collect-debug.sh
  if: failure()

Re-add success() when you add another condition

  1. When gating on a branch/event, keep the success gate: if: success() && github.ref == 'refs/heads/main'.
  2. Use failure() for steps that should only run after an earlier failure.
  3. Reserve always() for steps that must run even when the run is canceled.

How to prevent it

  • Prefer !cancelled() over always() unless you truly want cancel included.
  • Remember adding any if: removes the implicit success() gate - re-add it if needed.
  • Use failure()/success() explicitly for conditional cleanup and notifications.

Frequently asked questions

What causes "if: always() misuse"?
always() means run regardless of status - including cancellation. To run after success or failure but skip cancellation, use !cancelled() instead.
How do I fix if: always() misuse?
Combine status functions explicitly so the step runs in exactly the intended states.

Related guides

References

Latchkey auto-heals failures like this one - detected, fixed, and retried without you. Start free → 30-day trial · No credit card