Workflow Files, YAML Structure, Jobs, Steps, and Actions: Core Concepts and Mental Model
Chapter 01 established the event-to-run lifecycle. Chapter 02 now opens the workflow file itself. The goal is not to memorize YAML keys; it is to understand how a versioned document becomes an executable graph with scopes, runner boundaries, ordered steps, action dependencies, shell behavior, and observable conclusions. Once those boundaries are explicit, syntax errors and design mistakes become much easier to diagnose.
Learning objectives
-
Read a workflow from
.github/workflowsas executable structure rather than as a flat YAML document. -
Distinguish workflow-level configuration, job IDs and display
names, runner selection, ordered steps,
runscripts, andusesaction dependencies. - Explain which configuration can apply at workflow, job, or step scope and why the most local applicable setting matters.
- Explain why steps inside one job share a runner filesystem while separate hosted jobs do not share that filesystem.
- Inspect a small workflow before execution and predict its job graph, runner requirements, token permissions, and final evidence.
1. From Chapter 01's event to Chapter 02's executable document
When a supported event selects a workflow, GitHub does not execute
YAML line by line as though it were a shell script. It first
interprets a structured workflow document. Top-level keys describe
the workflow's identity and broad behavior. The
jobs map defines schedulable units. Each job declares a
runner and contains ordered steps. A step either asks a shell to
execute commands with run or asks the runner to execute
an action with uses.
This distinction is operationally important. A malformed workflow can fail before a run exists. A valid workflow can create jobs that wait for runners. A job can start and then fail in one step. Those outcomes belong to different layers, so the first debugging question should be “How far did the document get?” rather than “What YAML should I change?”
2. Mental model: structure becomes a runner-scoped execution graph
flowchart TD A[.github/workflows/ch02.yml] --> B[Top-level name / on / permissions / defaults] B --> C[jobs map] C --> D[Job ID: inspect] C --> E[Job ID: test] D --> F[Runner for inspect] E --> G[Runner for test] F --> H[Ordered run / uses steps] G --> I[Ordered run / uses steps] H --> J[inspect conclusion + logs] I --> K[test conclusion + logs] J --> L[workflow conclusion] K --> L
The vertical boundaries are as important as the boxes. Workflow-level configuration describes shared policy or defaults. A job is a schedulable boundary: it receives one runner and its own environment. Steps are sequential inside that job and can see changes made in the same job's workspace. Another hosted job receives another fresh runner, so ordering two jobs does not magically copy files between them.
3. Anatomy of a small workflow
name: Chapter 02 structure
on:
workflow_dispatch:
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
inspect:
name: Inspect structure
runs-on: ubuntu-24.04
steps:
- name: Show execution identity
run: |
printf 'run=%s attempt=%s\n' "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT"
printf 'workspace=%s\n' "$GITHUB_WORKSPACE"
test:
name: Independent second job
runs-on: ubuntu-24.04
steps:
- name: Show independent runner
run: |
printf 'job=%s runner=%s\n' "$GITHUB_JOB" "$RUNNER_NAME"
Read this from the outside inward. name is a
human-facing workflow name. on declares eligibility for
manual dispatch. permissions narrows the workflow
token. defaults.run.shell applies only to
run steps unless a more specific setting overrides it.
Under jobs, inspect and
test are stable job IDs; their name values
are display labels. Each job separately asks for an Ubuntu 24.04
hosted runner.
4. IDs are interfaces; names are presentation
| Element | Example | Why it matters |
|---|---|---|
| Workflow file path | .github/workflows/ch02.yml |
Versioned source that GitHub discovers as a workflow. |
| Workflow name | Chapter 02 structure |
Human-facing label in the Actions UI; not the same as file path. |
| Job ID | inspect |
Machine-facing identifier used by features such as
needs; keep it stable.
|
| Job name | Inspect structure |
Human-facing display text; can be clearer than the ID. |
| Step name | Show execution identity |
Creates readable logs and makes first-failure evidence easier to locate. |
| Step ID | id: metadata |
Optional machine-facing identifier needed when another expression reads that step's outputs. |
Names should make the run understandable to a reviewer. IDs should be short, stable, and meaningful because later workflow features refer to them. Renaming a display label and renaming a job ID are therefore different kinds of change.
5. run and uses cross different execution
boundaries
A run step gives command text to a shell on the runner.
A uses step invokes an action package identified by a
repository/path and reference. The latter is executable dependency
code, not decorative YAML. That is why Chapter 02 records exactly
which action revision was selected and why later security chapters
treat dependency governance as a first-class concern.
| Step form | Who supplies executable behavior? | Primary configuration | Key evidence |
|---|---|---|---|
run |
Your workflow's shell script |
shell, working-directory,
environment
|
Expanded step log, exit status, runner OS/toolchain. |
uses |
The referenced action |
Immutable action reference plus with/env
|
Exact owner/repository/SHA, action runtime, inputs, log. |
@v7 is convenient but mutable; record the
upstream release next to the immutable SHA for maintainability.
6. Scope: where a setting begins and ends
GitHub Actions deliberately supports configuration at several
scopes. A workflow-level env value can be visible to
jobs and steps unless overridden. A job-level env can
narrow or replace it for that job. A step-level value is narrower
still. Likewise, defaults.run can be set for the
workflow or a job, and the more specific applicable setting wins.
Permissions can also be specified at workflow or job scope.
Scope is not merely convenience. It controls blast radius. A token permission required by one publishing job should not automatically be granted to every read-only build job. A working directory used by one job should not silently redirect every script in the workflow.
7. Runner boundary: steps share; jobs isolate
For a standard GitHub-hosted runner, GitHub provisions a fresh runner instance for each job. Steps within that job execute on the same instance and can observe files created by earlier steps. A second job is a different schedulable unit with another fresh runner. The second job therefore cannot rely on the first job's uncommitted workspace files.
jobs:
producer:
runs-on: ubuntu-24.04
steps:
- run: echo "created in producer" > marker.txt
- run: cat marker.txt # works: same job, same runner
consumer:
runs-on: ubuntu-24.04
steps:
- run: cat marker.txt # fails: different job, fresh runner
Chapter 13 will introduce artifacts for file transfer. For now, the important concept is simply that a job dependency or ordering relationship is not a filesystem relationship.
8. Shell and working directory are part of the execution contract
Shell defaults are runner-OS dependent. A script written casually
for Bash may fail on a Windows job whose normal command environment
differs. The chapter's executable labs pin Ubuntu and set
shell: bash so that the command language is explicit.
When portability matters, either write portable commands or make
runner and shell choices visible in configuration.
defaults:
run:
shell: bash
working-directory: ./tools
GitHub currently does not allow contexts or expressions in
defaults.run. If a directory is dynamic, use an
explicit step-level working-directory or restructure
the workflow rather than pretending the default is runtime data.
9. Read-only inspection before the first run
Before committing a workflow, review it as a dependency and state manifest:
-
Is it under
.github/workflowsand named clearly? - Which events can select it?
- Which token permissions are requested?
- What job IDs and dependencies will exist?
- Which runner labels and shells are required?
- Which steps are scripts and which invoke external action code?
- Are external action references immutable?
- Which files are assumed to exist before checkout or generation?
- What evidence will prove each job's conclusion?
This review catches architecture mistakes that a generic YAML parser cannot: a document can be valid YAML yet still violate GitHub's workflow schema or your delivery policy.
10. Why this matters in DevOps
A workflow file is operational code. It encodes dependency execution, credentials, machine selection, side effects, and evidence. Treating it as an executable graph makes code review more meaningful: reviewers can reason about blast radius, parallelism, runner trust, reproducibility, and rollback before a run exists.
Knowledge check
Why can a workflow be valid YAML but still be invalid GitHub Actions configuration?
Generic YAML syntax only proves the document parses as YAML; GitHub also applies a workflow schema and execution semantics such as valid job keys, step placement, and runner requirements.
What does needs: producer guarantee about the
consumer job?
It creates a job dependency/order and exposes supported dependency result/output data; it does not make the producer runner filesystem appear on the consumer runner.
Why is a full action commit SHA stronger than
@v7 for reproducibility?
A full commit SHA identifies immutable repository content, whereas a tag can move. Record the human-readable release separately so maintainers still know what the SHA represents.
Where should you put a permission needed by only one mutation job?
Prefer the narrowest applicable scope—typically that job—rather than granting it to every job at workflow scope.
A Bash command works in one Ubuntu job and fails after moving the job to Windows. Which layer changed?
The runner/shell execution environment changed. YAML validity did not guarantee cross-platform shell compatibility.
Official references and version notes
- Understanding GitHub Actions — current definitions and execution relationships for workflows, jobs, steps, actions, and runners.
- Workflow syntax for GitHub Actions — authoritative workflow structure, jobs, steps, permissions, defaults, runners, and shell behavior.
-
Setting default shell and working directory
— current precedence and restrictions for workflow/job
defaults.run. - Using GitHub-hosted runners — job-to-runner isolation and filesystem sharing within a job.
- Secure use reference — current guidance to pin action dependencies to full-length commit SHAs and minimize privileges.
- actions/checkout v7.0.1 — verified upstream release corresponding to the immutable checkout SHA used later in this chapter.
- actions/setup-python v7.0.0 — verified upstream release corresponding to the immutable setup-python SHA used later in this chapter.
Version-sensitive behavior was rechecked against primary GitHub
documentation and GitHub-maintained action repositories on
2026-09-09. Executable examples use
ubuntu-24.04,
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
(upstream release v7.0.1), and where Python setup is needed
actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
(upstream release v7.0.0) with Python 3.13. At verification time
both actions declare a Node 24 runtime. Runner images, action
releases/runtimes, workflow keys, parser diagnostics, and
plan-dependent behavior can change; re-resolve current immutable
SHAs before copying these examples into long-lived production
workflows. This first lesson is mostly read-only and conceptual;
no external service, secret, package publication, deployment, or
paid feature is required.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.