2.03 Cancelling and Skipping Workflow Runs
Overview
Not every triggered run should finish, and not every matching event deserves a run at all. Cancelling stops work already in flight; skipping prevents it from starting.
Abstract
By default a job fails as soon as one of its steps fails, and a failing job cancels the rest of
the workflow — behavior you can override later with continue-on-error, if: always(), and
fail-fast. Runs can also be cancelled by hand from the run page or the CLI. Skipping works
differently: all matching events normally start a workflow, but push and pull_request
events honor a commit-message annotation such as [skip ci], which suppresses the run
entirely.
Why It Matters in Production
Cancelling is about not paying for work you already know is worthless. A run that will fail in twelve minutes because of a typo you spotted thirty seconds after pushing is twelve minutes of runner time and twelve minutes of waiting for everyone behind it in the queue.
Skipping is about not starting worthless work at all. A comment, a changelog line, a README fix — none of it changes behavior, and running the full test-and-deploy pipeline for it costs minutes and, on paid plans, money.
The caution with both: a cancelled or skipped run produces no status check. If that check is required for merging, the pull request stops dead.
Key Concepts
Cancelling
| Behavior | Default |
|---|---|
| A step fails | The job fails |
| A job fails | Remaining and dependent jobs are cancelled |
| Manual cancellation | Available from the run page's Cancel workflow button |
| Overrides | continue-on-error, if: always(), fail-fast: false, timeout-minutes |
Skipping
| Behavior | Detail |
|---|---|
| Default | Every matching event starts a workflow |
| Exception | push and pull_request events only |
| Mechanism | A keyword in the commit message |
| For pull requests | The latest commit on the head branch decides |
| Keywords | [skip ci], [ci skip], [no ci], [skip actions], [actions skip] |
Where the keyword goes
The annotation must appear in the commit message's first line or in the two-line block after a blank line — not buried in the middle of a long body. Square brackets are part of the syntax.
Skipping does not apply to every event
Only push and pull_request honor skip keywords. A workflow_dispatch, schedule, or
repository_dispatch run ignores the commit message entirely.
Common Use Cases
- Cancel a run you know will fail, instead of waiting for it to prove it
- Cancel a hung or runaway job consuming runner minutes
[skip ci]for comment-only, README, or changelog commits[skip ci]on automated commits a bot pushes back into the repository, to avoid trigger loopsconcurrencywithcancel-in-progressso a new push cancels the superseded run automaticallytimeout-minutesto cancel anything that exceeds its expected duration
Example Configuration or Commands
Skipping a run through the commit message:
# This push matches the workflow's triggers but starts no run
git commit -m "added comments [skip ci]"
git push origin main
# Equivalent keywords
git commit -m "docs: fix typo [ci skip]"
git commit -m "chore: bump version [no ci]"
git commit -m "comment only [skip actions]"
git commit -m "formatting [actions skip]"
Cancelling from the CLI instead of the run page:
# Find the in-flight run
gh run list --status in_progress
# Cancel it
gh run cancel <run-id>
# Watch what happens
gh run watch
Preventing a bot commit loop
A workflow that commits back to the repository — a version bump, a generated changelog — will retrigger itself on push. Annotating the bot's commit breaks the cycle:
- name: Commit generated files
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add dist/
git commit -m "chore: rebuild assets [skip ci]"
git push
GITHUB_TOKEN don't trigger workflows anyway, but the annotation
makes the intent explicit and still protects you when a PAT is used instead.
Best Practices
Best Practices
- Cancel early. The moment you know a run is pointless, stop it — the queue behind it is shared with the rest of the team.
- Set
concurrencywithcancel-in-progress: trueon CI workflows so rapid pushes don't pile up; leave it off on deploy workflows where a half-finished deployment is worse than a redundant one. - Put
timeout-minuteson every job. The platform default is generous, and a hung job silently consumes the allowance. - Prefer
paths-ignoreover per-commit[skip ci]for anything predictable, like docs directories — it works without discipline from contributors. - Reserve
[skip ci]for one-off judgment calls, and say in the commit message why. - Use
continue-on-errorfor genuinely optional steps rather than letting a flaky linter fail the build. - Add
if: always()to notification and cleanup jobs so a failure still reports.
Security Best Practices
Security
[skip ci]is a bypass available to anyone who can push. On a branch whose pipeline runs security scanning, a contributor can skip the scan with a commit message. Make the scan a required status check, or run it on ascheduleas well, so skipping cannot hide a change.- A skipped run reports no status at all — it is not a passing check. Branch protection must treat "no run" as "not approved", which is the default; do not configure around it.
- Avoid
[skip ci]on commits that touch.github/workflows/. Workflow edits are exactly what you want validated. - Cancelling a deploy mid-run can leave a partial deployment. Make deploy steps idempotent and prefer a rollback path over cancellation.
cancel-in-progresson a deploy workflow can abort a release halfway. Scope the concurrency group carefully, and considercancel-in-progress: falsefor production environments.- Review who cancels production runs — cancellation is available to anyone with write access, and the run log records the actor.
Do and Don't
| ✅ Do | ❌ Don't |
|---|---|
| Cancel runs you know will fail | Let a doomed run occupy the queue |
Use [skip ci] for comment/doc-only commits |
Use it to dodge failing tests |
Prefer paths-ignore for predictable skips |
Rely on everyone remembering an annotation |
Set timeout-minutes per job |
Assume a hung job will stop itself |
Use cancel-in-progress on CI |
Apply it blindly to deploy workflows |
| Put the keyword in the commit's first line | Bury it in the middle of a long message |
Mark optional steps continue-on-error |
Let a flaky step fail the whole pipeline |
Common Mistakes
Common Mistakes
- Keyword in the wrong place. Mid-body text isn't recognized; use the first line or the first two lines after a blank line.
- Missing brackets.
skip ciwithout square brackets does nothing. - Expecting skip to work on other events. Scheduled and manually dispatched runs ignore it.
- Skipping on a PR and expecting the merge to proceed. With no run there is no status check, so required checks stay pending.
- Annotating the wrong commit in a PR. The latest commit on the head branch is what counts.
- Thinking cancelled means passed. A cancelled run is neither success nor failure, and required checks treat it as not satisfied.
- Assuming a failed job stops everything. Independent parallel jobs keep running; only dependent jobs are cancelled.
- Using
cancel-in-progresson deployments. The superseded run is killed mid-deploy. - Amending a message after pushing. The keyword is read at push time; rewriting history afterwards requires another push.
Breaks in production
[skip ci] on a commit that looked cosmetic but wasn't ships untested code to main. The
pipeline shows no red run — it shows no run at all, which is easy to read as "nothing to worry
about" during review.
Troubleshooting
# Confirm the exact commit message that was pushed
git log -1 --pretty=%B
# Did a run start for that commit?
gh run list --limit 5 --json headSha,event,status,conclusion
# Cancel the current in-flight run
gh run list --status in_progress --json databaseId --jq '.[0].databaseId' | xargs gh run cancel
# Which runs were cancelled recently, and by what?
gh run list --limit 20 --json conclusion,displayTitle --jq '.[] | select(.conclusion=="cancelled")'
| Symptom | Likely cause |
|---|---|
| No run after a push | Skip keyword in the commit message, or a path/branch filter excluded it |
| Skip keyword ignored | Wrong placement, missing brackets, or a non-push/pull_request event |
| PR cannot merge after skipping | Required status check has no run to satisfy it |
| Run cancelled without anyone clicking | cancel-in-progress superseded it, or timeout-minutes elapsed |
| Dependent job shows "cancelled" | An upstream job failed — expected default behavior |
| Notification job never ran | Missing if: always() |
| Workflow retriggers itself endlessly | Bot commit pushes without [skip ci] and uses a PAT |
Tip
Cancellation is not instantaneous. GitHub sends a stop signal and gives steps a grace period to
shut down, so a long-running step may continue briefly. Cleanup steps marked if: always()
still get a chance to run.
Quick Recap
- A step failure fails the job; a job failure cancels the rest of the workflow by default.
- Those defaults are adjustable with
continue-on-error,if: always(), andfail-fast. - Runs can be cancelled manually from the run page or with
gh run cancel. - All matching events start a workflow by default.
pushandpull_requestare the exceptions: a commit-message keyword skips the run.- Keywords:
[skip ci],[ci skip],[no ci],[skip actions],[actions skip]. - For a pull request, the latest commit's message decides.
- A skipped or cancelled run produces no status check, which blocks required checks.
paths-ignore,concurrency, andtimeout-minutesare the structural equivalents of skipping and cancelling.
Interview / Revision Notes
Quick revision
Q: What happens by default when a step fails? The job fails, and the rest of the workflow is cancelled.
Q: How do you let a step fail without failing the job?
continue-on-error: true on the step.
Q: How do you cancel a run manually?
The Cancel workflow button on the run page, or gh run cancel <run-id>.
Q: How do you skip a run that would otherwise be triggered?
Put a keyword such as [skip ci] in the commit message.
Q: Which events honor skip keywords?
push and pull_request only.
Q: Name the accepted keywords.
[skip ci], [ci skip], [no ci], [skip actions], [actions skip].
Q: For a pull request, which commit message is checked? The latest commit on the head branch.
Q: How do you cancel superseded runs automatically?
A concurrency group with cancel-in-progress: true.
Q: Why can skipping block a merge? No run means no status check, and a required check stays pending.
This topic can be expanded into sub-pages:
continue-on-errorand job-level failure handling,fail-fastin matrices, concurrency groups and deployment queues, timeouts and runner cost control, and required status checks with skipped runs.