Workflow Anatomy
Every workflow file has the same skeleton, and once you can read it, you can read anyone's CI. Here's a complete, real workflow — we'll take it apart piece by piece:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
💡 The Bottom Line
A workflow (one YAML file) fires on an event (on:) and
contains jobs; each job gets a fresh runner VM and runs
steps in order. A step either uses: a published action or
run:s a shell command. That's the whole vocabulary.
The hierarchy, top to bottom
name:— what the Actions tab displays.on:— the trigger. Here: any push tomain. (Lesson 3 is entirely about this key.)jobs:— the units of parallel work. One here (test— the name is yours to choose); multiple jobs run simultaneously by default, each on its own fresh VM. Sequencing requires an explicitneeds: teston the dependent job — that's how build→test→deploy chains are expressed.runs-on:— which VM image:ubuntu-latest(default choice, cheapest),windows-latest,macos-latest.steps:— executed in order, on the same machine, sharing the filesystem. First step failed? The rest are skipped and the job is red.
uses vs run
Two kinds of step, visually distinct:
uses: actions/checkout@v4— invoke a published, reusable action.with:passes its parameters. The@v4pins the version — never omit it.run: npm test— execute a shell command, exactly as if typed on the runner. Multi-line scripts use YAML's pipe:run: |then indented lines.
And the non-obvious essential: the runner starts empty — your code is not
there until actions/checkout clones it. That's why practically every job on
earth starts with that step; forgetting it is everyone's first red run ("no such file
package.json").
🙋 There Are No Dumb Questions
Q: Do my steps share anything between them? Do my jobs?
A: Steps: yes — same VM, same working directory, so files written by npm ci
are there for npm test. Jobs: no — each job is a separate fresh machine.
Passing files between jobs takes explicit upload-artifact/download-artifact
steps (Lesson 5), and that distinction — steps share, jobs don't — resolves about half of
all beginner confusion.
⚠️ Watch Out
YAML indentation is structure, and workflow files punish it: an extra two spaces turns a step into gibberish, and the error surfaces as "workflow file invalid" at the top of the Actions tab — or worse, the workflow silently not appearing at all. Use an editor with YAML support, keep indentation to consistent 2-space levels, and when a workflow "doesn't exist," check the Actions tab for a parse-error banner first.
🔧 Try It Yourself
Write the minimal workflow by hand — no starter template. Create
.github/workflows/hello.yml in a scratch repo:
name: Hello
on: push
jobs:
greet:
runs-on: ubuntu-latest
steps:
- run: echo "Hello from a runner"
- run: uname -a && node --version
Push it, open the Actions tab, and read the logs — note that Node was already installed (runners come with common toolchains preloaded) and that the machine is a throwaway Ubuntu VM you never had to create.
✅ Quick Check
Why does nearly every job begin with uses: actions/checkout@v4?
Next up: everything that can go after on: — pushes, PRs, cron schedules, and
the manual-run button.