What Is the Difference Between a Job and a Step?
A job gets its own runner and its own billed minutes, while steps run in order on that one machine, so runner size, parallelism, and cost are set per job.
A job is the unit that GitHub Actions schedules onto its own runner, and steps are the ordered commands that run on that one machine inside a job. That boundary is why runs-on, machine size, parallelism, and billing are all decided per job, while steps only decide the order of work on a machine the job already holds.
Answer
Jobs run in parallel by default and are serialized only by a needs edge, while the steps inside a job always run one after another on the same runner (GitHub workflow syntax). Everything that costs money or wall-clock time attaches to the job.
| Property | Job | Step |
|---|---|---|
| Machine | Gets its own runner, selected by runs-on | Runs on the runner the job already holds |
| Parallelism | Runs alongside other jobs unless needs adds an edge | Runs in sequence, one at a time |
| Billing | The billed unit: minutes are counted per job | No separate charge, its time is inside the job's minutes |
| State | Starts clean, and the runner is destroyed when the job ends | Shares the working directory, installed tools, and $GITHUB_ENV |
| Failure | A failed job blocks every job that lists it in needs | A failed step ends the job unless continue-on-error is set |
| Config surface | runs-on, strategy, container, services, environment | uses, run, with, env, if |
The billing row is the one that surprises people. GitHub counts Actions minutes per job rather than per step, and rounds each job's run time up to the nearest minute (GitHub Actions billing, checked 2026-08-13).
So five steps on one machine bill as one job's minutes. A job that runs actions/checkout, setup-node, npm ci, npm run build, and npm test for a total of 20.0 minutes on warp-ubuntu-latest-x64-4x at $0.008 per minute costs $0.160 per run, whatever the step count. Turning those five steps into three jobs does not divide that number, it adds runner boots and repeated setup work to it.
Detail
The cost of a job boundary, in minutes
Take the workload above and assume these step durations: checkout 0.2 minutes, setup-node 0.5, npm ci 2.3, lint 0.8, build 4.2, and a 12.0 minute test suite. Every layout below runs the same work on warp-ubuntu-latest-x64-4x at $0.008 per minute (cloud runners documentation, pricing).
| Layout | Jobs | Billed minutes per run | Cost per run | Wall clock |
|---|---|---|---|---|
| All six steps in one job | 1 | 20.0 | $0.160 | 20.0 min |
| Lint split into a parallel job | 2 | 23.0 | $0.184 | 19.2 min |
| Lint, build, and four test shards | 6 | 35.0 | $0.280 | 13.2 min |
Each new job repeats checkout, setup-node, and npm ci, which is 3.0 minutes of setup that the single-job layout paid once. The lint split buys 0.8 minutes of wall clock for 3.0 billed minutes. The sharded layout buys 6.8 minutes of wall clock for 15.0 billed minutes, which at 500 pull request runs a month is $60.00 more on the invoice and 3,400 minutes less waiting on results. Both trades can be correct, and both are decisions about jobs.
When to split steps into a second job
Split when the work can run at the same time as everything else, when it needs a different machine, or when it should be retried without repeating the rest. (cloud runners documentation), so a different machine per job is a label change.
name: ci
on:
pull_request:
branches: [main]
jobs:
lint:
runs-on: warp-ubuntu-latest-x64-2x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
build-and-test:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- run: npm test
package-macos:
needs: build-and-test
runs-on: warp-macos-latest-arm64-6x
steps:
- uses: actions/checkout@v4
- run: ./scripts/package-macos.shlint earns its own job because it shares no state with the build and can run on a smaller, cheaper label at the same time. package-macos earns one because it needs a macOS runner, which no step inside a Linux job can reach.
build and test stay in one job for the opposite reason: npm test reads the dist directory that npm run build wrote to the same working directory. Moving npm test into its own job would start it on a clean runner and force an artifact upload and download to carry dist across the boundary, plus a second npm ci. The guide to sharing data between jobs covers what that copy costs for job outputs, artifacts, and caches.
Reading the boundary in your own numbers
The Reports section makes the unit explicit. In the CI billing tab, each row is a single job execution showing repository, job name, runner label, execution time, billed time, and cost, and the Jobs tab aggregates duration and queue time per unique repository, workflow, and job name combination (reports documentation). There is no per-step row, because there is no per-step charge.
That is also the shape of the tuning loop. Sort the CI billing table by cost to find the job carrying the bill, check its duration percentiles in the Jobs tab, and then decide whether the fix is a smaller label, a bigger label, or a different job layout. The guide to speeding up GitHub Actions works through those levers in order, and every cost number on this page comes from published per-minute rates with the date they were checked. Signup includes $10 free credits, which is enough Linux minutes to run both layouts and compare the billed totals yourself.
Related Questions
Does adding more steps to a GitHub Actions job cost more?
Only through the minutes those steps take. A job is billed for its own run time at the rate of the runner label it uses, so five steps on one machine bill as one job's minutes rather than five separate charges. Adding a second job adds a second runner and a second billed duration, including the checkout and dependency install that job has to repeat. Current per-minute rates for every label are on the pricing page.
When should I split GitHub Actions steps into a separate job?
Split when the steps can run at the same time as the rest of the workflow, when they need a different runner label or operating system, or when they should be retried on their own. Keep steps together when each one depends on files the previous step wrote, because a second job starts on a clean runner and has to copy that state back through artifacts, outputs, or a cache. The guide to speeding up GitHub Actions covers the parallelism side, and choosing a runner size covers the label each of those jobs should carry.
Do steps in a GitHub Actions job share the same filesystem?
Yes. Every step in a job runs on the runner that job was assigned, so the working directory, installed tools, environment written to $GITHUB_ENV, and Docker daemon state all carry from one step to the next. Nothing survives the end of the job, since the runner is torn down once the last step finishes. The mechanisms for moving state past that boundary are in the guide to sharing data between GitHub Actions jobs.
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.