How Do I Skip a Workflow for a Commit?

Put [skip ci] in the commit message to skip the push and pull request runs that commit would trigger, or gate one job with an if expression on the message.

Answer

Put one of five documented markers anywhere in the commit message and the runs that commit would have triggered never start: [skip ci], [ci skip], [no ci], [skip actions], or [actions skip] (GitHub skip workflow runs documentation, checked on 2026-08-13). When only part of the pipeline should stop, leave the run alone and gate the individual job with an if expression on the commit message, which keeps the run present so required checks still report.

The marker form covers the whole trigger. There is a second syntax for the same effect, a skip-checks trailer placed at the end of the message after two blank lines, written as either skip-checks: true or skip-checks:true. Git strips consecutive blank lines from a message by default, so the trailer form needs git commit --cleanup=verbatim to survive (GitHub skip workflow runs documentation, checked on 2026-08-13).

git commit -m "docs: fix broken link in README [skip ci]"

A skipped run starts no machine, so it consumes no runner minutes and reaches no invoice. WarpBuild rates are per minute on the pricing page, and a run that never starts bills nothing on any of them.

Detail

Which triggers the marker actually covers

Skip instructions apply to the workflow runs that the commit containing them would trigger, and to nothing else. GitHub scopes them to two events.

TriggerSkipped by a marker in the commit message
pushYes
pull_requestYes
pull_request_targetNo
scheduleNo
workflow_dispatchNo
workflow_run, repository_dispatch, and other eventsNo

Two consequences follow from the scoping rule. A nightly schedule workflow runs on the same code the next morning whatever the last commit message said, so a marker never hides a build from a cron job. And a workflow on pull_request_target keeps running, which matters because that trigger is the one that carries write-scoped credentials on fork pull requests (GitHub skip workflow runs documentation, checked on 2026-08-13).

The marker also travels with the commit. GitHub scans the message of the commit that triggers the run, so rebasing a branch, squashing it, or cherry-picking a commit carries the marker text into whatever message the new commit ends up with (GitHub skip workflow runs documentation, checked on 2026-08-13). That is the source of most surprise skips on a default branch.

Gating one job instead of the whole run

The whole-run skip is blunt. Most teams that reach for it want one specific stage to stop, usually an end-to-end suite or a deploy, while lint and unit tests keep reporting. Read the message once in a cheap guard job and let the expensive jobs depend on its output.

name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  guard:
    runs-on: warp-ubuntu-latest-x64-2x
    outputs:
      run_e2e: ${{ steps.check.outputs.run_e2e }}
    steps:
      - id: check
        env:
          PUSH_MESSAGE: ${{ github.event.head_commit.message }}
          PR_TITLE: ${{ github.event.pull_request.title }}
        run: |
          MESSAGE="${PUSH_MESSAGE:-$PR_TITLE}"
          if [[ "$MESSAGE" == *"[skip e2e]"* ]]; then
            echo "run_e2e=false" >> "$GITHUB_OUTPUT"
          else
            echo "run_e2e=true" >> "$GITHUB_OUTPUT"
          fi

  unit:
    runs-on: warp-ubuntu-latest-x64-8x
    steps:
      - uses: actions/checkout@v5
      - run: npm ci
      - run: npm test

  e2e:
    needs: [guard, unit]
    if: needs.guard.outputs.run_e2e == 'true'
    runs-on: warp-ubuntu-latest-x64-16x
    steps:
      - uses: actions/checkout@v5
      - run: npm ci
      - run: npm run test:e2e

Three details in that file are worth copying. The commit message goes into an environment variable before bash touches it, because interpolating an author-controlled string straight into a run: block is the script injection shape GitHub warns about (secure use reference, checked on 2026-08-13). The guard job holds warp-ubuntu-latest-x64-2x, the smallest Linux size at $0.004 per minute (cloud runners documentation), so the check itself is close to free. And github.event.head_commit exists only on push events, which is why the script falls back to the pull request title.

For a workflow that only ever runs on push, the one-line form is enough:

  deploy:
    if: ${{ !contains(github.event.head_commit.message, '[skip deploy]') }}

contains casts its arguments to strings and matches case-insensitively (expressions reference, checked on 2026-08-13), so [SKIP DEPLOY] matches too, and an absent head_commit reads as an empty string and lets the job run.

The third option is a path filter. paths-ignore on the trigger keeps the marker out of commit messages entirely and decides from the diff instead, which is the durable answer for docs-only changes. Running a job only when files change covers that path, including the file-count and rename edge cases.

What a skipped run is worth

Markers pay off on repositories where a large share of commits touch only documentation, changelogs, or generated fixtures. Take a pipeline of four jobs at 7 minutes each, so 28 runner minutes per push, and 60 docs-only pushes a month. Rates come from the cloud runners documentation and the pricing page, checked on 2026-08-13.

Runner labelShapePer minute1,680 minutes a month
warp-ubuntu-latest-x64-2x2 vCPU, 8 GB$0.004$6.72
warp-ubuntu-latest-x64-8x8 vCPU, 32 GB$0.016$26.88
warp-windows-latest-x64-8x8 vCPU, 32 GB$0.032$53.76
warp-macos-latest-arm64-6x6 vCPU, 22 GB$0.08$134.40

The spread across that column is the whole decision. Skipping 60 pushes on a small Linux runner returns $6.72 a month, which is not worth a governance argument. Skipping the same pushes on a macOS pipeline returns $134.40 a month, and a matrix that fans one push across Linux, Windows, and macOS returns the sum. Price the marker against the platform the skipped jobs actually hold rather than against the cheapest row.

Working out GitHub Actions cost per pull request turns the same arithmetic into a per-pull-request figure, and re-run storms and what they cost covers the opposite problem, where the same commit runs many times.

The commit that needed the checks

The failure mode is a commit that carries a marker and should not have. Someone tags a hotfix [skip ci] at 2am to get past a flaky test, a rebase drags a marker from a docs commit onto a code commit, or a squash merge composes its message from branch commits that each carried one. No run is created, so nothing appears in the Actions tab for that commit and no alert fires on a failure that never happened.

The rule that closes it is branch protection rather than review discipline. Require a pull request before merging and require the status checks to pass. A workflow skipped by a commit message leaves its check in a pending state, and a pending required check blocks the merge until a commit without the marker pushes the workflow through (troubleshooting required status checks, checked on 2026-08-13). The marker then costs an extra commit rather than an unverified merge. Direct pushes to the protected branch are the gap that matters, because a [skip ci] commit landing outside a pull request faces no check at all.

GitHub's own guidance on the same page is to avoid requiring workflows that can be skipped. In practice that means keeping the required check in a workflow with no path filters and no marker handling, and letting that workflow fan out into the jobs you are willing to gate, which is exactly what the guard pattern above does.

One diagnostic note. A skipped run and a stuck run look different. A marker means no run exists at all. A run that exists with a job sitting at queued is a runner availability problem, usually bot repository access or a runner group restriction, and the common issues documentation walks through both checks. git log --grep='\[skip ci\]' --since=90.days on the default branch is the audit that tells you how often the marker is reaching code that needed the checks.

Does a skip marker stop every workflow on the commit?

No. It stops the runs that the commit would trigger through push and pull_request events. Workflows listening on pull_request_target, schedule, workflow_dispatch, and every other event still run (GitHub skip workflow runs documentation, checked on 2026-08-13). The scope is also per commit: the next commit without a marker triggers everything normally.

Why is my pull request stuck after I pushed a commit with a skip marker?

Because the required check never reported. A workflow skipped by a commit message leaves its check pending, and a pending required check blocks the merge (troubleshooting required status checks, checked on 2026-08-13). Push any follow-up commit whose message carries no marker, or amend the message and force-push the branch, and the workflow runs.

How do I skip one job instead of the whole run?

Let the run start and gate the job. A one-line if expression with contains works when the workflow only listens on push; the guard job above covers pull_request as well by falling back to the pull request title. Either way the run exists, so required checks report and the merge is not blocked. Where the decision depends on the diff rather than the message, running a job only when files change is the better mechanism.

Skip markers cut runs that should never have started, and everything left over is a speed problem. Speeding up GitHub Actions covers the rest of the levers, from runner size to caching.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.