Skip to main content

14 — YAML without tears

Read this first: YAML counts spaces and guesses types, and both behaviours will bite you. This lesson makes its rules predictable so that workflows and compose files stop being a place where you edit by trial and error. Twenty minutes here saves hours later.

Time: about 30 minutes. No prerequisites beyond a text editor.

Two rules, then the traps​

Rule 1: indentation is structure, and it must be spaces. Tabs are not allowed anywhere. Two spaces per level is the convention everything in this repository uses.

Rule 2: YAML guesses the type of every unquoted value. This is the source of every surprise below.

The shapes​

A map is key/value pairs:

name: Deploy
runs-on: ubuntu-latest

A list uses -:

branches:
- main
- staging

Nest them and you get everything you have ever seen in a workflow:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Say hello
run: echo hi

The one thing to read carefully is the list of steps. Each - starts a new item, and the keys that belong to that item line up with the key that followed the dash. Above, name and run are both part of the second step because run is indented to the same column as name.

Multi-line strings use |, which keeps newlines:

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

That is how nearly every non-trivial workflow step is written, and it means the block is a shell script — everything you learned in lessons 01 to 03 applies inside it.

Break it on purpose: the type guessing​

Predict: write down what each of these values becomes.

a: yes
b: no
c: 'no'
d: 22
e: '22'
f: 1.10
g: on
country: NO
version: 3.10
time: 12:30

Now the truth, from a real YAML parser:

{
"a": true,
"b": false,
"c": "no",
"d": 22,
"e": "22",
"f": 1.1,
"g": true,
"country": false,
"version": 3.1,
"time": 750
}

Four of those deserve a moment:

  • country: NO is false. This is famously called the Norway problem — a country-code list written in YAML loses Norway. yes, no, on, off, true, false and several capitalisations are all booleans.
  • version: 3.10 is 3.1. It was read as a number, and trailing zeros in numbers are not significant. Your version string has silently changed.
  • time: 12:30 is 750. YAML read it as base-60: 12×60 + 30. This is a real feature that nobody wants.
  • 'no' and '22' stayed strings, because quoting turns the guessing off.

The rule that follows: quote anything that is meant to be a string, especially version numbers. node-version: '22' and python-version: '3.10' are quoted in every well-written workflow for exactly this reason.

Break it on purpose: the tab​

Put a real tab character where spaces belong:

jobs:
build:
steps:
- run: echo hi
yaml.scanner.ScannerError: while scanning for the next token
found character '\t' that cannot start any token
in "tab.yml", line 4, column 1

Good — the parser is explicit and the line number is right. Configure your editor to insert spaces for YAML files and this can never happen again.

Break it on purpose: valid YAML, wrong meaning​

This is the failure that actually costs time, because nothing errors.

jobs:
build:
steps:
- run: echo hi

steps is indented two spaces — the same level as build, not inside it. Predict: does this parse? What structure does it produce?

{
"jobs": {
"build": null,
"steps": [
{ "run": "echo hi" }
]
}
}

It parses perfectly. But build is now null — a job with no content — and steps has become a sibling job rather than the contents of one. GitHub will reject this with a message about an unexpected value, and the line it points at is often not the line you need to fix.

When a workflow file is rejected, check indentation before anything else, and distrust the reported line number. The parser reports where it noticed the problem, which is usually after where you made it.

Reading your own file back​

The fastest way to check what a file means, rather than what you meant:

docker compose config # for compose files: fully resolved

For workflows there is no local equivalent built in, so the practical move is to keep the structure shallow and consistent, and to copy the shape of a file that already works — which is what .github/workflows/ is for.

Comments and anchors​

Comments are # to end of line. Use them; every workflow in this repository is heavily commented, and several of those comments are the only record of why something is the way it is.

YAML also has anchors (&name / *name) for reusing a block. Do not use them in GitHub Actions. .github/workflows/docs.yml repeats its path filters twice rather than anchoring them, with a comment explaining why: GitHub does not reliably expand anchors in every position, and a silently dropped filter would mean documentation pull requests stop being checked at all — a failure nobody would notice.

Recap​

  • Spaces only, never tabs, and indentation is the entire structure.
  • YAML guesses types. no is false, 3.10 is 3.1, 12:30 is 750. Quote version numbers and anything meant to be a string.
  • Wrong indentation often produces valid YAML with the wrong meaning, and the reported error line is frequently not the line to fix.

Next: 15 — Your first workflow, where YAML starts running commands on somebody else's computer.