Each job is its own VM
A job is a unit of work that runs on a single runner (a VM or container). Jobs in the same workflow run in parallel by default — that's the main reason you split work into multiple jobs.
Important consequences:
- Each job starts with a clean working directory. Files created in one job are not visible to another. Pass artifacts between jobs explicitly with
upload-artifact+download-artifact. - Each job's runner is a fresh VM. Caches must be re-hydrated.
- Jobs can declare
needs:to wait on other jobs and form a DAG. - Jobs can declare
if:conditions to skip based on context.
Anatomy of a job
runs-on— required. The runner label (ubuntu-latest,macos-14,self-hosted, etc.).steps— required. The list of commands or actions, run in order on this runner.needs— optional. Other jobs this one depends on.if— optional. Expression that gates the whole job.strategy— optional. Matrix expansion (we'll cover later).env— optional. Job-scoped env vars.outputs— optional. Values published to dependent jobs.environment— optional. Production-style gating with approvals.