GitHub Actions Jobs and Steps
Jobs and steps are the core execution units of any GitHub Actions workflow. Jobs define what work gets done and where. Steps define the individual commands that make up each job. Mastering how jobs and steps work together gives you full control over how your automation runs.
Jobs — The Work Containers
A job is a named block inside the jobs section of your workflow. Each job runs on its own runner machine, completely isolated from other jobs.
jobs:
build: ← Job 1
runs-on: ubuntu-latest
steps: ...
test: ← Job 2
runs-on: ubuntu-latest
steps: ...
The names build and test are identifiers you choose. GitHub uses them to display results in the Actions tab and to allow other jobs to reference them.
Parallel vs Sequential Jobs
By default, all jobs in a workflow run at the same time — in parallel. This is the fastest approach when jobs do not depend on each other.
Workflow starts
│
├──── build job starts (runs on its own runner)
│
└──── lint job starts (runs on its own runner)
When one job must finish before another begins, use the needs keyword to create a dependency:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."
test:
needs: build ← Wait for build to finish first
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
deploy:
needs: test ← Wait for test to finish first
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
This creates a pipeline where each job waits for the previous one to succeed:
build → test → deploy
Waiting for Multiple Jobs
A job can wait for several jobs to complete before it starts:
deploy:
needs: [build, lint, security-scan]
The deploy job starts only after all three jobs in the list succeed.
Job Outputs
Jobs can pass data to other jobs using outputs. This is useful when one job produces a value (like a version number) that another job needs.
jobs:
generate-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- name: Get version
id: get-version
run: echo "version=1.4.2" >> $GITHUB_OUTPUT
deploy:
needs: generate-version
runs-on: ubuntu-latest
steps:
- run: echo "Deploying version ${{ needs.generate-version.outputs.version }}"
Steps — The Individual Instructions
Steps live inside a job and execute one after another from top to bottom. If a step fails, the job stops and marks all remaining steps as skipped (unless you configure otherwise).
The run Step
The run key executes shell commands directly on the runner:
steps:
- name: Install packages
run: npm install
- name: Run multiple commands
run: |
echo "Starting build"
npm run build
echo "Build complete"
The pipe symbol | lets you write multi-line commands. Each line runs in sequence.
The uses Step
The uses key calls a pre-built action from the GitHub Marketplace or a local file:
steps:
- name: Download repository code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
The with key passes input parameters to the action. Think of it like filling in a form that the action reads.
Step IDs
Each step can have an optional id field. This lets other steps reference the output of this step:
steps:
- name: Run tests and capture output
id: run-tests
run: echo "result=pass" >> $GITHUB_OUTPUT
- name: Show result
run: echo "Test result was ${{ steps.run-tests.outputs.result }}"
Continuing After a Failure
Use continue-on-error: true on a step to allow the workflow to keep running even if that step fails:
steps:
- name: Optional lint check
run: npm run lint
continue-on-error: true ← Failure here does not stop the job
- name: Run tests
run: npm test ← This still runs even if lint failed
Working Directory
By default, all steps run in the root directory of the repository. You can change the working directory for a specific step:
steps:
- run: npm install
working-directory: ./frontend
Or set a default working directory for all steps in a job:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
steps:
- run: npm install
- run: npm test
Job and Step Summary
Feature | Job | Step
----------------------|-------------------|---------------------------
Runs on | Its own runner | Same runner as its job
Execution order | Parallel by default | Always sequential
Can depend on others | Yes (needs) | No (always top to bottom)
Can pass data | Yes (outputs) | Yes (GITHUB_OUTPUT)
Can fail independently| Yes | Yes (continue-on-error)
