The issue opens pre-filled with this page title and URL.
Are you repeating the same lines in several jobs?extends and YAML anchors let you factor out your configuration. Define it once, reuse it everywhere.
The DRY principle
DRY = Don't Repeat Yourself. It is a basic principle of programming: if you copy-paste code, it should probably be factored out.
In CI/CD, if you have 5 jobs with the same Docker image and the same cache, you have 5 places to edit when you change versions. With extends, you edit a single place.
This guide is for you if...
Read this guide through the section that matches your need. If you
duplicate configuration (same image, same cache in five jobs), start
with extends, job inheritance. To inherit
while changing some parts, see
overriding properties. If you know
YAML, the anchors cover the advanced cases. And if
you hesitate between the two, the comparison
extends vs anchors vs !reference
settles it.
What you will learn
By the end of this module, you will be able to:
Use extends: make a job inherit from a hidden template
Create hidden jobs: the . convention for templates
Override properties: customize the inheritance without rewriting everything
The diagram below shows what the child job takes from the parent, and what it
can redefine for itself. The dotted arrows mark the properties inherited
as they are (greyed out in the child), the solid arrows the ones that
build_prodoverrides with its own value.
.base_job is a hidden job: it does not run, only build_prod does.Source · SVG
Basic syntax
A job can inherit from another job with extends. The parent job is usually hidden (prefixed with .) so that it does not run itself:
build inherits from .base AND .with_cache. The final result:
build:
image: node:18@sha256:c6ae79e38498325db67193d391e6ec1d224d96c693a8a4d943498556716d3783# From .base
variables:
ENV: production# From .base
cache:
paths:
- .npm/# From .with_cache
script: npm run build# Defined locally
A template can itself inherit from another template: the inheritance
chains. GitLab accepts up to 11 levels of inheritance, but the
documentation recommends no more than 3: beyond that, you have to climb
the whole chain to find out where a value comes from.
Overriding properties
Inheritance would be limited if you could not customize. The child job can override (replace) any property inherited from the parent:
image: node:20@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5# Replaces node:18 with node:20
script:
- npm run build# Replaces npm test with npm run build
variables:
NODE_ENV: production# Replaces development with production
# Keeps almost everything, changes only the script
test:
extends: .node_base
script:
- npm run lint# Replaces the script
- npm test
What is overridden vs what is inherited:
Property in build_prod
Value
Source
image
node:20
Overridden (was node:18)
script
npm run build
Overridden (was npm test)
variables.NODE_ENV
production
Overridden (was development)
Swipe to see more
Merging arrays: the classic trap
Here is the classic trap with extends: arrays (lists) are completely replaced, not merged. The GitLab documentation words it this way: extends merges hashes (tables), but not arrays (lists).
.base:
script:
- echo "step 1"
- echo "step 2"
job:
extends: .base
script:
- echo "only this"# WARNING: REPLACES completely, does not add to it!
Result:job runs onlyecho "only this". The "step 1" and "step 2" steps have disappeared.
How do you add steps without replacing?
There are two solutions. The first: put the extra command in a different
key from the parent's. Here the parent has no after_script, so the
child job can add one without losing anything from the parent:
.base:
before_script:
- echo "setup"# Always runs first
script:
- echo "main"# The main script
job:
extends: .base
after_script:
- echo "cleanup"# Key absent from the parent: added at the end
Execution order:setup → main → cleanup
Be careful: before_script is also a list. If job redefined it, the
parent's version would be lost, exactly as with script. The second solution,
valid for any list, is !reference, presented
below: it copies the parent's list before you
add your own commands.
Anchors are a native YAML feature: they work in any YAML file, not only in a .gitlab-ci.yml. Their syntax is detailed in the guide Anchors, aliases and multiple documents in YAML (in French); this section keeps only what serves a .gitlab-ci.yml.
An anchor exists only in its own file. GitLab reads each YAML file
separately, then assembles the include files. An anchor defined in an
included file is therefore not found from the file that includes it, and
the reverse is true as well. The hidden job that serves as the source of a
*name reference must stay in the same file as the jobs that use it. To
share across files, use extends or !reference, compared
below.
Syntax: three symbols to know
Symbol
What it does
&name
Defines an anchor (like "copy")
*name
References the anchor (like "paste")
<<:
Merges the content into a dictionary
Swipe to see more
A concrete example
Here is how to use anchors to avoid repetition:
# 1. Define an anchor with &
.job_template: &job_config # &job_config = "I name this block"
Several anchors can be merged into the same job. Order matters, and it is
the opposite of extends: for a key present in two anchors, it is the
first one in the <<: [*a, *b] list that wins, and a key written in the
job itself wins over all the anchors. The merge is also shallow: if both
anchors carry a cache: table, the first one replaces it entirely instead of
merging it key by key.
The variables block merges like any other mapping. A job can therefore
inherit from a common set while adding its own, without having to copy the
inherited values.
variables: &global_vars
VAR1: "value1"
VAR2: "value2"
job:
variables:
<<: *global_vars
VAR3: "value3"# Adds a variable
extends vs anchors vs !reference
Aspect
extends
Anchors
!reference
Readability
Clearer
Cryptic syntax
Explicit
Merge mappings
Yes, deeply
Shallow
No (insertion)
Reuse lists
No, the parent's list is overwritten
No, the list is overwritten
Yes
Multi-file
Yes, with include
No, same file only
Yes, with include
GitLab-specific
Yes
No (YAML)
Yes
Recommended for
Complete jobs
Simple values
Fragments (rules, script)
Swipe to see more
!reference: reusing fragments
GitLab provides !reference to reuse configuration fragments (lists, mappings) without the limitations of anchors:
The four mistakes below are classic traps of this technique. The first two are
silent: an expected job does not appear, or commands disappear
without any message. The last two block the creation of the
pipeline with an explicit error.
1. The job does not run
Symptom: Your job does not appear in the pipeline.
Cause: Its name starts with ., so it is a hidden job (template).
Solution: Remove the . from the name, or create a job that extends this template.
# ❌ Will never run
.build:
script: npm run build
# ✅ Will run
build:
script: npm run build
2. Script replaced instead of extended
Symptom: The parent's commands have disappeared.
Cause: Arrays (lists) do not merge, they are replaced.
Solution: Redefine the whole script, or copy the parent's list with !reference [.base, script]. A key absent from the parent, like after_script in the example above, is added without overwriting anything.
3. extends: unknown job
Symptom: GitLab refuses to create the pipeline.
Cause: The parent job does not exist (typo, file not included).
Solution: Check:
The exact name of the parent job (with the . if it is a template)
That the file containing the parent is actually included (include:)
4. Anchor not found
Symptom: YAML error when the pipeline is created.
Cause: You reference an anchor (*name) before defining it (&name), or the anchor is defined in another file (an include file).
Solution: Always define the anchor before using it, in the same file. If the hidden job lives in an included file, replace the anchor with extends.
The lab "Stop repeating the pipeline, without changing what it does" replays
this guide on a GitLab CE forge of your own, set up on a virtual
machine with its runner and its registry, rather than on gitlab.com: no
minute quota, no identity verification, and a project you can break as
often as you need.
Two jobs declare the same image and the same cache, word for word. You
factor them with a hidden job extended by extends (sections "extends:
job inheritance" and "Overriding properties") and a YAML anchor merged by
<<: on a block of variables (section "Extending variables"), keeping the
behaviour exactly as it was. It is the only lab of the training graded on
the file: a factorisation resolved by GitLab leaves no trace in the
pipeline state, so the grading checks that the file carries an extended hidden
job and a merged anchor, then that the jobs still run and that the coverage is
still reported.
One trap this page does not show: coverage: belongs to the job, not to
the cache or to the image. If you move the test job into a hidden job and
forget that line, the pipeline stays green and the coverage disappears
without a word. The coverage: keyword is described in Quality
reports.
The lab starts with dsoxlab run gl-13-factoriser-le-yaml, after dsoxlab provisionprepares the virtual machine, its runner and its registry.
Installing DSOXLab and its prerequisites are described in the DSOXLab
documentation.
Key takeaways
extends provides job inheritance, prefer it to anchors
Hidden jobs (.name) do not run, they serve as templates
Mappings merge, scalars and arrays are overwritten
Multiple inheritance: the last one wins in case of conflict; 3 levels of inheritance recommended, 11 at most
Anchors (&, *, <<:) for advanced cases, valid in their own file only; with <<: [*a, *b], the first anchor wins
!reference to reuse fragments (rules, script) between jobs
Combine with include to share between projects, as the lesson include and templates details
Test your knowledge
Knowledge check
Check what you have learned with this interactive quiz
10 questions
5 min
70% to pass
How it works
The timer starts when you click Start
Multiple choice, true/false and short answer questions
You can move between questions
Detailed results are shown at the end
Starts the quiz and the timer
⏱️--:--
0 / 10
Review
(0/0)
--:--
0%
0Correct
0To review
0:00Time
-Level
Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.