Chapter 02Lesson 01~100 minutes

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.

Workflow anatomyJobs & stepsrun vs usesScopesRunner boundary

Learning objectives

  • Read a workflow from .github/workflows as executable structure rather than as a flat YAML document.
  • Distinguish workflow-level configuration, job IDs and display names, runner selection, ordered steps, run scripts, and uses action 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

Workflow structure and execution boundaries
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.
Supply-chain rule: production examples in this course use full commit SHAs for external actions. A major-version tag such as @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/workflows and 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.

Next lesson

Build the graph one boundary at a time

Lesson 2 starts from an empty file, adds shell steps, then introduces immutable checkout/setup actions and a second independent job.

Knowledge check

Why can a workflow be valid YAML but still be invalid GitHub Actions configuration?

What does needs: producer guarantee about the consumer job?

Why is a full action commit SHA stronger than @v7 for reproducibility?

Where should you put a permission needed by only one mutation job?

A Bash command works in one Ubuntu job and fails after moving the job to Windows. Which layer changed?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.