Structuring a GitHub Actions File: Workflows, Jobs and Steps
Structure an efficient GitHub Actions file into workflows, jobs and steps; parallelize with needs, scope each concern, and keep pipelines easy to review.
Netbay Engineering
Netbay Engineering
On this page
A GitHub Actions file can be a two-line convenience or a sprawling build system. The difference is discipline about the three levels of abstraction the format gives you: the workflow, the job, and the step. Once you pin down what belongs at each level, your pipelines become easier to read, faster to reason about, and far simpler to debug. This post walks through that layering with realistic YAML you can adapt to any repository, and the same structure maps cleanly onto the self-hosted runners you might attach to a Netbay VPS.
The workflow: the whole file
The workflow is the outermost container. It has a name, a trigger, and any global permissions or defaults. It also declares the jobs. You should think of the workflow as the "when and what" — when should anything run, and what are the major chunks of work to perform.
Keep global concerns here: concurrency limits to avoid stampedes, permissions for the GITHUB_TOKEN, and a default shell. Everything inside a workflow is reusable across the jobs it contains.
The job: a unit of environment
A job runs on its own runner. That runner has an operating system, a fresh copy of the repository checkout, and its own set of containers or services. A job is where you choose the platform: ubuntu, windows, macos, or a self-hosted label. Each job runs in parallel with other jobs unless you add the needs keyword.
Think of a job as "one thing done in one environment." Linting on Linux, building on Windows, and testing a matrix across Node versions are each separate jobs. If something does not need a particular OS, do not split it into another job just to parallelize trivial work — jobs that cannot run in parallel on the same machine actually cost more.
The step: an atomic action
Steps are the smallest unit, and they run sequentially inside a job, in the order listed. If one step fails, later steps are skipped by default unless you mark them with continue-on-error or an if condition. A step either runs a run script or uses an action via uses.
Steps should be small and single-purpose: install dependencies, run lint, run tests, upload results. When a step fails you can quickly see exactly which command failed, which keeps debugging fast.
A minimal but complete example
Putting it together, here is a workflow that validates a repository with two jobs running in parallel:
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-22.04
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm testSequencing jobs with needs
By default jobs run in parallel. To force ordering, add needs with the names of the jobs that must finish first. GitHub Actions builds a dependency graph from needs declarations. Here the test job will not start until lint finishes successfully:
jobs:
build:
runs-on: ubuntu-22.04
steps:
- run: echo building
test:
runs-on: ubuntu-22.04
needs: build
steps:
- run: echo testing
deploy:
runs-on: ubuntu-22.04
needs: [build, test]
if: github.ref == 'refs/heads/main'
steps:
- run: echo deployingA naming and scoping checklist
Before you write a workflow, run each concern through the three levels. Triggers, concurrency, permissions, and defaults belong at the workflow level. A chosen OS, a set of services, and parallel units of environment belong at the job level. Commands, checkout, cache, and artifact upload belong at the step level. If you find yourself duplicating the same five steps in every job, that is a signal to consider a composite action or a reusable workflow — both covered later in this series.
Takeaway
Structure before speed. Put triggers and policy at the workflow level, environment at the job level, and single-purpose commands at the step level. A clean hierarchy is easier to review, easier to cache, and far easier to debug when a build goes sideways. When you want a fast self-hosted runner to speed up these jobs, you can spin up an Ubuntu VPS on Netbay in under 60 seconds and wire it in with a runner token — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance