Skip to content

Anatomy of a Workflow File


A workflow file is just YAML — but YAML has opinions, and GitHub Actions has its own vocabulary layered on top of it. Get the shape of this file right in your head now, and every workflow you write after this will feel like filling in a template rather than guessing at syntax.

Where Workflow Files Live

Every workflow file lives in .github/workflows/ at the root of your repository, and it ends in .yml or .yaml — both work, pick one and stay consistent. GitHub scans that entire directory: every file in it is a separate, independent workflow. You can have five files in there and five completely unrelated workflows running on their own triggers.

actions-practice/
└── .github/
    └── workflows/
        ├── hello.yml
        └── (more workflows go here)

The directory doesn’t exist in your practice repository yet — you’ll create it in the next chapter when you write your first real workflow.

The YAML Habits That Actually Matter Here

You don’t need to become a YAML expert, but four habits will save you most of the debugging pain:

  • Indentation is structure, not decoration. YAML uses indentation to express nesting, the same way Python does. Two spaces per level is the near-universal convention in GitHub Actions examples, and it’s what you’ll see throughout this course.
  • Never use tabs. YAML’s spec forbids tabs for indentation outright. A stray tab character will fail the whole file, and it’s one of the most common “why won’t my workflow even show up” causes.
  • key: value pairs, and lists with -. Nested settings are key: on one line with children indented below it. Lists — like a series of steps — use a leading - for each item.
  • Quote strings that could be misread. Anything containing a colon, starting with a special character, or that looks like a number/boolean when it’s meant to be text should be wrapped in quotes. A cron schedule like 30 5 * * * or a version string like 3.10 are classic cases where quoting avoids surprises.

The Skeleton Of A Workflow

Every workflow file, no matter how complex it eventually gets, is built from the same four levels of nesting:

    flowchart TD
    A[Workflow file] --> B["on: (the trigger)"]
    A --> C[jobs]
    C --> D["job (e.g. test)"]
    D --> E["runs-on (which machine)"]
    D --> F[steps]
    F --> G["step 1"]
    F --> H["step 2"]
    F --> I["step N"]
  
  • The workflow has a name and an on trigger — what event kicks it off.
  • jobs is a map of one or more jobs, each identified by an id you choose (like test or build).
  • Each job declares runs-on — which machine runs it — and a list of steps.
  • Each step either runs a shell command (run) or calls a reusable action (uses) — the next chapter goes deep on that distinction.

An Annotated Example

Here’s a workflow file with every key explained inline. Don’t create this file yet — you’ll build a real, working version of something very close to it in the next chapter.

name: CI                      # Shown in the Actions tab; optional, but always name your workflows

on:                            # The trigger — what event starts this workflow
  push:                        # Run whenever someone pushes a commit
    branches: [main]           # ...but only if it lands on the main branch

jobs:                          # A workflow is made of one or more jobs
  greet:                       # "greet" is this job's id — you choose this name
    runs-on: ubuntu-latest     # Which GitHub-hosted machine runs this job
    steps:                     # A job is a list of steps, run in order
      - name: Say hello        # Optional, human-readable label shown in logs
        run: echo "Hello!"     # A shell command this step executes

Every one of those keys — on, jobs, runs-on, steps, run — is something you’ll type dozens of times over the rest of this section, so it’s worth having this shape settled before moving on.

Common Gotchas

Warning

Indentation errors are the #1 cause of “invalid workflow file” errors. If GitHub refuses to even recognize your workflow — no run shows up in the Actions tab at all — check indentation first. A step accidentally nested one level too deep or too shallow is usually the culprit.

Tip

Multi-line run commands: | vs >. When a step needs to run several shell commands, you can write them as a block:

- run: |
    echo "first line"
    echo "second line"

The | character preserves line breaks exactly as written — each line runs as its own command, which is almost always what you want for shell scripts. The > character instead folds everything into a single line with spaces in between, which quietly breaks multi-command scripts. If your multi-line run step mysteriously only executes the first command, check whether you used > where you meant |.

Note

The on: key and YAML’s boolean quirk. Older YAML tooling (following the YAML 1.1 spec) treats bare words like on, off, yes, and no as booleans unless they’re quoted. GitHub’s own workflow parser handles on: correctly — your workflows won’t break because of this. But if you ever open a workflow file in a generic YAML linter or formatter and see it silently rewritten to true:, this is why. It’s a tooling quirk to be aware of, not a bug in your file.

You’ll see the ${{ }} syntax scattered through real-world workflow files too — that’s how you reference dynamic values like a commit’s author or a secret. It’s not something you need yet; it’ll get its own proper treatment later in this course. For now, just recognize it on sight so it doesn’t look like a typo.

Last updated on