| | |

YAML 06 ๐Ÿ“„ DRY Patterns โ€” Anchors (&), Aliases (*), and Merge Keys (<<)

โฑ๏ธ Estimated read: 10 min | |

YAML’s DRY (Don’t Repeat Yourself) features let you define a block of configuration once and reuse it in multiple places. An anchor (&) marks a node with a name; an alias (*) references that node elsewhere; a merge key (<<) inserts the entries of one mapping into another. Together they eliminate duplicated configuration in large files, but each has rules and pitfalls that determine whether the reuse works as intended.

This chapter covers the three mechanisms, their syntax, and the precedence rules that decide which value wins when merged keys conflict. It explains why anchors cannot cross document boundaries or file includes, how merge keys interact with existing keys, and the parser-specific behavior that can silently reverse the expected precedence. The sections that follow walk through anchors and aliases, the merge key and its ordering rules, and the limitations that make some reuse patterns invalid.

Key point: An anchor marks a node for reuse, an alias references it, and << merges a mapping’s entries into another; keys defined in the current mapping override merged keys, and keys from earlier sources in a merge sequence override later ones.


Why DRY patterns exist

The duplication problem. A CI configuration with ten jobs that share the same image, services, and before_script repeats those keys in every job. When the image changes, every job must be edited, and a missed job runs a different environment. Anchors and merge keys let the shared block be defined once and referenced by every job, so a change happens in one place .

The anchor scope problem. Anchors are scoped to the document in which they appear. An anchor defined in one document of a multi-document stream is not visible in the next document. In tools like GitLab CI, anchors cannot cross file boundaries when include is used; the documentation recommends !reference tags or the extends keyword for cross-file reuse . The scope rule is a limitation, not a bug: each document is parsed independently, and an alias that references an anchor from another document is an error.

The merge precedence problem. The YAML merge key specification states that keys in mapping nodes earlier in a sequence override keys specified in later mapping nodes . This means <<: [*BIG, *LEFT, *SMALL] gives BIG‘s keys precedence over LEFT‘s, which take precedence over SMALL‘s. A PyYAML regression in 2026 changed this behavior to last-wins for multi-character keys, and the issue documents that all existing tests used single-character keys that happened to work because of Python string interning . The precedence rule is specified but not universally implemented correctly.


a. Anchors and aliases

An anchor is written as &name before a node. The name can contain letters, digits, and some punctuation; it must be unique enough within the document to avoid unintended references. An alias is written as *name and refers to the most recent preceding node with that anchor .

# Define an anchor on a mapping
defaults: &default_settings
  timeout: 30
  retries: 3

# Reference it with an alias
service_a:
  settings: *default_settings
# service_a.settings is the same mapping as defaults

# Anchors on scalars and sequences
host: &primary_host "db.example.com"
backup_host: *primary_host

shared_tags: &tags
  - production
  - critical

The alias is a reference, not a copy. In most parsers, the aliased value is the same object as the anchored node, so mutating one in memory affects the other. When serialized back, the alias is written as *name, not as a duplicated block.

An anchor on a sequence can be used inside another sequence, which is how GitLab CI shares script blocks across jobs .

.some-script: &some-script
  - echo "Execute this script second"
  - echo "Execute this script too"

job1:
  script:
    - *some-script
    - echo "Execute something for this job only"

b. The merge key <<

The merge key inserts the entries of one or more mappings into the current mapping. The syntax is <<: *alias for a single mapping, or <<: [*alias1, *alias2] for a sequence of mappings.

# Single mapping merge
.job_template: &job_configuration
  image: ruby:2.6
  services:
    - postgres

test1:
  <<: *job_configuration
  script:
    - test1 project
# test1 has image, services, and script

# Multiple mapping merge
base_a: &a
  kk: 1
  x: 10
base_b: &b
  kk: 2
  y: 20

merged:
  <<: [*a, *b]
  label: combined
# kk is 1 (from *a, earlier in sequence)
# x is 10, y is 20, label is "combined"

The precedence rules are two-layered. First, keys explicitly defined in the current mapping override keys from the merged mapping. In the example above, if test1 also had image: node:20, that value would win over ruby:2.6 from the merge. Second, when merging a sequence of mappings, keys from earlier mappings in the sequence override keys from later mappings . This is the opposite of what many developers expect from “last wins” semantics in other configuration systems.

The merge key only performs a shallow merge. Nested mappings are not recursively combined; if the current mapping and the merged mapping both have a services key, the current mapping’s value replaces the merged value entirely, including any nested structure .


c. Limitations and parser differences

Anchors cannot cross document boundaries. In a multi-document stream separated by ---, an anchor defined in the first document is not available in the second .

# Document 1
defaults: &d
  timeout: 30
---
# Document 2
service:
  <<: *d  # ERROR: anchor not defined in this document

Anchors also cannot cross file boundaries when a tool uses an include mechanism. GitLab CI’s documentation is explicit: anchors are only valid in the file they were defined in, and !reference or extends must be used for cross-file reuse . This is a tool-level limitation, not a YAML specification limitation, but it determines what reuse patterns are valid in practice.

The merge key is defined in YAML 1.1’s type repository, not the core specification. YAML 1.2 made the merge key optional, and not every parser enables it by default. PyYAML supports it in safe_load; YAML::PP in Perl requires explicitly enabling the Merge schema, and notes that with the schema enabled, a plain scalar << is interpreted as a merge key everywhere in the document, so a literal << must be quoted . A parser that does not enable the merge key treats << as an ordinary key name.

PyYAML’s precedence behavior is also worth checking. A 2026 issue documents a regression in the main branch where merge precedence changed from first-wins to last-wins for multi-character keys . The stable release (6.0.3 and earlier) implements the specification correctly; the regression is in unreleased code. A project that relies on merge precedence should pin the PyYAML version and test the behavior explicitly.


Complete Example Session

# ============================================
# PART 1: ANCHOR ON A MAPPING
# ============================================
.defaults: &defaults
  timeout: 30
  retries: 3

# ============================================
# PART 2: ALIAS REFERENCE
# ============================================
service_a:
  settings: *defaults
# settings is the same mapping: { timeout: 30, retries: 3 }

# ============================================
# PART 3: MERGE KEY WITH OVERRIDE
# ============================================
service_b:
  <<: *defaults
  timeout: 60
# timeout is 60 (local overrides merged), retries is 3

# ============================================
# PART 4: MULTIPLE MERGE SOURCES
# ============================================
.left: &left
  x: 0
  y: 2
.big: &big
  r: 10
  y: 99

combined:
  <<: [*left, *big]
  label: example
# x: 0 (from left), r: 10 (from big)
# y: 2 (left is earlier in sequence, overrides big's 99)

# ============================================
# PART 5: SHALLOW MERGE (NESTED NOT COMBINED)
# ============================================
.base: &base
  nested:
    a: 1
    b: 2

override:
  <<: *base
  nested:
    c: 3
# nested is { c: 3 }, not { a: 1, b: 2, c: 3 }

# ============================================
# PART 6: THE PARSED MENTAL MODEL
# ============================================
# combined: { x: 0, y: 2, r: 10, label: "example" }
# override: { nested: { c: 3 } }

The parts covered a mapping anchor, an alias reference, a merge with a local override, a multiple-source merge with precedence, the shallow-merge limitation, and the parsed results.


Quick Reference

Syntax

SymbolNamePurpose
&nameAnchorMark a node for reuse
*nameAliasReference an anchored node
<<Merge keyInsert a mapping’s entries

Merge Precedence

RuleDetail
Local keysOverride merged keys
Sequence orderEarlier mappings override later ones
DepthShallow only; nested mappings replaced, not combined

Scope Limitations

LimitationDetail
Document boundaryAnchors do not cross ---
File includeAnchors do not cross file includes (tool-dependent)
YAML versionMerge key is 1.1 type, optional in 1.2
Parser defaultNot all parsers enable merge key by default

Best Practices

โœ… Do This:

# Define anchors on hidden keys with a leading dot
.defaults: &defaults
  timeout: 30                                                # โœ…

# Use merge key for shared mapping entries
service:
  <<: *defaults                                              # โœ…

# Check merge precedence with a test
# (earlier source wins in a sequence)                         # โœ…

# Quote a literal << if the merge schema is enabled
literal: "<<"                                                # โœ…

โŒ Don’t Do This:

# Reference an anchor from another document
---
a: &x 1
---
b: *x  # error: anchor not in this document                   # โŒ

# Expect deep merge
<<: *base
nested: { c: 3 }  # replaces, not merges                     # โŒ

# Assume cross-file anchors work in CI
# (use extends or !reference instead)                        # โŒ

# Rely on merge precedence without pinning parser version
# (regression exists in PyYAML main)                         # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Unknown anchor errorAnchor defined in another document or fileDefine anchors per document
Merge doesn’t combine nested keysShallow merge onlyRepeat nested keys or restructure
Precedence unexpectedEarlier-wins rule misunderstoodTest sequence order explicitly
<< treated as literal keyMerge schema not enabledEnable merge key or quote literal <<
PyYAML precedence regressionUnreleased main branch changePin stable version (6.0.3)

Real-World Examples

1. Mapping anchor

.defaults: &defaults
  timeout: 30

2. Alias reference

service: *defaults

3. Merge with override

<<: *defaults
timeout: 60

4. Multiple merge sources

<<: [*left, *big]

5. Sequence anchor

tags: &tags [prod, critical]

6. Alias in sequence

job_tags: *tags

7. GitLab template merge

<<: *job_configuration

8. Shallow merge replacement

nested: { c: 3 }  # replaces merged nested

9. Quoted literal merge key

"<<" : literal_value

10. Document-scoped anchor

# anchor must be in same document as alias

Visual

Anchor, Alias, Merge

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ANCHOR (&)                                             โ”‚
โ”‚  defaults: &defaults                                    โ”‚
โ”‚    timeout: 30        โ—„โ”€โ”€ marked for reuse              โ”‚
โ”‚    retries: 3                                           โ”‚
โ”‚         โ”‚                                               โ”‚
โ”‚         โ”‚  ALIAS (*)                                    โ”‚
โ”‚         โ–ผ                                               โ”‚
โ”‚  service: *defaults   โ—„โ”€โ”€ same mapping reference        โ”‚
โ”‚                                                         โ”‚
โ”‚  MERGE (<<)                                             โ”‚
โ”‚  job:                                                   โ”‚
โ”‚    <<: *defaults      โ—„โ”€โ”€ entries inserted              โ”‚
โ”‚    timeout: 60        โ—„โ”€โ”€ local overrides merged        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Merge Precedence

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  <<: [*a, *b]                                           โ”‚
โ”‚                                                         โ”‚
โ”‚  *a: { kk: 1, x: 10 }                                   โ”‚
โ”‚  *b: { kk: 2, y: 20 }                                   โ”‚
โ”‚                                                         โ”‚
โ”‚  Result: { kk: 1, x: 10, y: 20 }                        โ”‚
โ”‚              โ–ฒ                                          โ”‚
โ”‚              โ”‚                                          โ”‚
โ”‚         earlier source wins                              โ”‚
โ”‚                                                         โ”‚
โ”‚  Local keys override both:                              โ”‚
โ”‚  kk: 99  โ†’  { kk: 99, x: 10, y: 20 }                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Shallow Merge

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MERGE IS SHALLOW                                       โ”‚
โ”‚                                                         โ”‚
โ”‚  base: &base                                            โ”‚
โ”‚    nested: { a: 1, b: 2 }                               โ”‚
โ”‚                                                         โ”‚
โ”‚  override:                                              โ”‚
โ”‚    <<: *base                                            โ”‚
โ”‚    nested: { c: 3 }                                     โ”‚
โ”‚                                                         โ”‚
โ”‚  Result: nested = { c: 3 }                              โ”‚
โ”‚  NOT { a: 1, b: 2, c: 3 }                               โ”‚
โ”‚                                                         โ”‚
โ”‚  Nested mappings are replaced, not combined             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Scope Boundary

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ANCHORS ARE DOCUMENT-SCOPED                            โ”‚
โ”‚                                                         โ”‚
โ”‚  Document 1:                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”‚
โ”‚  โ”‚ a: &x 1                                  โ”‚           โ”‚
โ”‚  โ”‚ b: *x        โ—„โ”€โ”€ valid                   โ”‚           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚
โ”‚                                                         โ”‚
โ”‚  ---                                                    โ”‚
โ”‚                                                         โ”‚
โ”‚  Document 2:                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”‚
โ”‚  โ”‚ c: *x        โ—„โ”€โ”€ ERROR: anchor unknown   โ”‚           โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚
โ”‚                                                         โ”‚
โ”‚  Anchors do not cross document boundaries               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Anchor syntax&name before a node
Alias syntax*name references the anchor
Merge key<<: *alias or <<: [*a, *b]
Local precedenceCurrent mapping keys override merged
Sequence precedenceEarlier sources override later
Merge depthShallow only
Document scopeAnchors do not cross ---
File scopeAnchors do not cross includes (tool-dependent)
YAML versionMerge key is 1.1 type, optional in 1.2

Key takeaways:

  • Anchors mark nodes; aliases reference them; merge keys insert mappings. The three work together but serve different purposes: & defines, * references, << combines .
  • Merge precedence has two layers. Local keys override merged keys, and earlier mappings in a merge sequence override later ones. The second rule is the opposite of “last wins” and is often implemented incorrectly .
  • The merge is shallow. Nested mappings are replaced wholesale, not combined recursively. A deep merge requires a tool-specific extension, not the standard << .
  • Anchors are document-scoped. They cannot cross --- boundaries, and tool-level includes require a different reuse mechanism like extends or !reference .
  • Parser support varies. The merge key is defined in YAML 1.1’s type repository and is optional in 1.2. Not every parser enables it by default, and PyYAML’s main branch has a documented precedence regression .

Remember: Anchors and aliases are references; the merge key is an insertion. The reference shares the node, the insertion copies its entries into a new mapping with local keys taking priority. The merge is shallow, the scope is the document, and the precedence is earlier-wins. When reuse does not work, check whether the anchor is in the same document, whether the parser has merge keys enabled, and whether the expected nested combination requires a deep merge that the standard << does not perform.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!