YAML anchors
A YAML anchor is a label — &name — that marks a value so you can reuse
it later in the same document with an alias, *name. The parser
resolves aliases while reading, so your program receives plain data with
the value repeated. Anchors are YAML's own answer to copy-paste.
defaults: &defaults # anchor the mapping
image: node:22
retries: 2
timeout: 10m
unit:
<<: *defaults # merge every key from defaults
script: bun test
e2e:
<<: *defaults
timeout: 30m # local key wins over the merged one
script: bunx playwright test
notify_on: &channels ["#ci", "#releases"]
deploy_notify: *channels # alias: the same list again
After parsing, e2e is { image: node:22, retries: 2, timeout: 30m, script: … } and deploy_notify is the same two-item list.
Three pieces
| Syntax | Name | What it does |
|---|---|---|
&name |
anchor | Labels the value that follows it |
*name |
alias | Stands for the anchored value, in full |
<<: *name |
merge key | Copies the keys of an anchored mapping into the current mapping |
An alias reuses a value as is. You can't change one key through a plain alias. The merge key is what lets you take a mapping and override part of it.
How merges resolve
- Local keys always win. A key written in the mapping beats the same key
from
<<, whether it comes before or after the merge line. - Several merges: earlier wins. In
<<: [*base, *gpu], a key present in both comes frombase. - Merges are shallow. If
defaultshasenv: { A: 1 }and you setenv: { B: 2 }locally, the result is{ B: 2 }, not both. Nested maps are replaced, not combined. - Lists can't be merged.
<<only works with mappings. There is no YAML syntax to append to an anchored list.
Where anchors work, and where they don't
| Where | Anchors and aliases | <<: merge key |
Reuse across files |
|---|---|---|---|
| Docker Compose | yes | yes | include, extends |
| GitLab CI | yes | yes | extends, !reference (not anchors) |
| GitHub Actions | yes (since 2025) | no | reusable workflows, composite actions |
| Kubernetes manifests | yes | yes | Helm, Kustomize |
Helm values.yaml |
yes | yes | values files, _helpers.tpl |
Docker Compose pairs anchors with top-level x- keys, which Compose ignores,
so you have somewhere to put a shared block that isn't a service:
x-logging: &logging
driver: json-file
options: { max-size: 10m }
services:
api: { image: api:latest, logging: *logging }
worker: { image: worker:latest, logging: *logging }
Gotchas
- One document only. An anchor doesn't reach past a
---separator, or into another file. - Define before use. An alias must come after its anchor in the file.
- YAML 1.2 dropped the merge key. Most parsers (js-yaml, PyYAML, Go's
yaml.v3, SnakeYAML) still accept it, but a strict 1.2 parser may read
<<as an ordinary key. - Converting to JSON expands everything. JSON has no references, so aliases become copies and the anchors are gone for good.
- Alias bombs. Nested aliases can expand exponentially (the YAML version of the "billion laughs" attack). Parsers that read untrusted YAML cap alias expansion; check that yours does.
Seeing what an anchor actually expands to
When a merged CI job does something you didn't expect, the fastest check is to look at the expanded data. Comma's free YAML to JSON converter resolves every anchor, alias and merge key so you can see exactly what the tool receives. YAML diff compares two files semantically, so refactoring repeated blocks into anchors shows no changes if you got it right. Both run in your browser, and either result can be published as a link teammates comment on.
Try it
Comma is free: unlimited reports, unlimited commenters, unlimited revision history.
Open the YAML to JSON converter →