skip to content

When would you split a GitLab CI configuration into parent and child pipelines with `trigger:include`, and what do you give up by doing it?

level: principalimportance: nice to knowfreq 32%

answer

  1. a job that creates a pipeline instead of running
  2. runtime decisions static YAML cannot express
  3. the parent can go green over a failed child
  4. ordering does not cross the boundary
  5. the artifact becomes the source of truth

basics

~20 s

Split when one configuration has grown past what a team can reason about, or when the job set must be decided at runtime — a trigger job with include generates a separate child pipeline. The cost is a fragmented view, no cross-pipeline needs ordering, explicit status and variable propagation.

solid answer

~50 s

A `trigger:` job creates a **downstream pipeline** rather than running a script — `trigger:include` for a child pipeline in the same project, `trigger:project` for a multi-project one. The two cases that justify it: configuration that has become too large or too conditional to read as one file, and job sets that can only be known at runtime, where a job generates YAML into an artifact and a second job triggers `include: [{artifact: generated.yml, job: generate}]`. The costs are real. The trigger job reports success as soon as the downstream pipeline is created unless you set `strategy: depend`, so failures can go unnoticed. `needs:` cannot order jobs across pipelines — the cross-pipeline forms only fetch artifacts. Variables must be forwarded deliberately via `trigger:forward` or the job's `variables`. And debugging shifts from reading a file to reading generated output. Split when the config is genuinely unreadable or genuinely dynamic; not merely because it is long.

code

yaml · 13 lines
yaml
generate:
  stage: build
  script: ./scripts/generate-pipeline.sh > generated.yml
  artifacts:
    paths: [generated.yml]

run-generated:
  stage: test
  trigger:
    include:
      - artifact: generated.yml
        job: generate
    strategy: depend

go deeper

for a junior

Know that a trigger: job creates another pipeline rather than running a script, and that a child pipeline shows up as its own entry in the parent's graph.

for a middle

Explain the difference between trigger:include for a child in the same project and trigger:project for a multi-project pipeline, and what strategy: depend adds to the trigger job's status.

for a senior

Diagnose the classic failures: a parent green over a failed child, a child missing variables the parent had, artifacts that no longer arrive because needs: cannot cross the boundary. Know how a dynamic child is generated and published.

for a principal

Own the decision itself — whether the job set is knowable in advance, whether the real driver is ownership rather than file length, what fragmenting observability costs the team, and whether the migration is worth the delivery time it buys back.

## What the mechanism is A normal job runs a script on a runner. A **trigger job** does not: it tells GitLab to create another pipeline. Two forms matter here: ```yaml # child pipeline, same project backend: stage: build trigger: include: backend/.gitlab-ci.yml strategy: depend # multi-project pipeline downstream: stage: deploy trigger: project: platform/deployer branch: main strategy: depend ``` A child pipeline is a separate pipeline object with its own stages, its own job graph, and `$CI_PIPELINE_SOURCE == "parent_pipeline"`. It appears in the parent's graph as an expandable node, not as jobs inside the parent. The **dynamic** variant is what makes the feature powerful: one job writes YAML — from a component inventory, a template, whatever the repository knows at that moment — publishes it as an artifact, and the trigger job includes it from that artifact. ```yaml generate: stage: build script: ./scripts/generate-pipeline.sh > generated.yml artifacts: paths: [generated.yml] run-generated: stage: test trigger: include: - artifact: generated.yml job: generate strategy: depend ``` This is the only sanctioned way to decide the *set of jobs* from information computed during the run, because ordinary `rules:` are evaluated before anything executes. ## The two cases that justify the split **Genuine dynamism.** The job set depends on something the pipeline discovers — which components exist, what a manifest declares, how many shards a test suite needs. No amount of static configuration expresses that. **Comprehension and blast radius.** A configuration that no single person can hold in their head is a delivery risk regardless of how it is factored. Splitting along ownership boundaries lets a team change its own child pipeline without reviewing everyone else's rules, and confines a mistake to one child. Pipeline creation itself also gets cheaper: a child that is never triggered costs nothing to evaluate. Neither case is "the file is long". Length alone is usually a factoring problem inside one pipeline, not a reason for a second one. ## What you give up **Status propagation is opt-in.** Without `strategy: depend`, the trigger job goes green the moment the downstream pipeline is *created*. The parent can report success over a failed child. `strategy: depend` makes the trigger job mirror the downstream pipeline's status and wait for it. Forgetting it is the single most common parent-child bug. **Ordering does not cross the boundary.** `needs:` links jobs within one pipeline. There is no way to say "this child job runs after that parent job" beyond the trigger point itself. The cross-pipeline forms — `needs:pipeline:job` for a parent or upstream pipeline, `needs:project` for another project — download artifacts; they are not scheduling edges. Any sequencing you need must be expressed as trigger points, which is coarser. **Context must be forwarded.** A child does not automatically inherit everything. Variables defined on the trigger job pass down, and `trigger:forward` controls whether pipeline variables and YAML-defined variables are forwarded. Getting this wrong produces a child that runs with a subtly different environment from the parent — the hardest class of pipeline bug to see. **Observability fragments.** One pipeline is one page. A parent with six children is seven pages, seven durations, and a merge request widget that shows the parent's status. Nested children add another level, and GitLab limits how deep nesting may go. **Generated YAML is harder to review.** With a dynamic child, the artifact — not the repository — is the source of truth for what ran. Code review sees the generator, not its output. Teams that do this well publish the generated file as an artifact and lint it in the generating job, so at least the run is auditable after the fact. ## How to decide Ask three questions in order. Is the job set knowable before the pipeline starts? If yes, static configuration wins and there is nothing to discuss. If no, a dynamic child is the only mechanism that fits. If it is knowable but the configuration is unmanageable, ask whether the real problem is ownership — several teams editing one file — or just structure. Ownership is a legitimate reason to split into children along team lines; structure is better solved inside one pipeline. Then ask what the failure story looks like. If a broken child must fail the merge request, `strategy: depend` is mandatory and you should say so out loud. If a downstream deployment pipeline in another project should *not* block this one, the absence of `strategy: depend` is a deliberate choice, not an oversight — and it should be commented as such. ## The migration cost nobody budgets Splitting an established pipeline is not a refactor you finish in an afternoon. Every rule that referenced a job by name across the new boundary breaks, artifact paths change hands, and the team's mental model of "where do I look when it is red" has to be rebuilt. Do it when the current shape is actively costing delivery time, and do it along a boundary — ownership or generated-versus-static — that will still make sense in a year.

  • What does `strategy: depend` change about a GitLab CI trigger job?
    Without it, the trigger job succeeds as soon as the downstream pipeline is created, so the parent can report green while the child fails. With `strategy: depend`, the trigger job waits for the downstream pipeline and mirrors its status, so a child failure fails the parent. Use it whenever the downstream result must gate anything upstream.
  • Can a job in a child pipeline declare `needs:` on a job in the parent pipeline?
    Not for ordering. `needs:` schedules within a single pipeline. The cross-pipeline forms — `needs:pipeline:job` for a parent or upstream pipeline and `needs:project` for another project — only download artifacts from an already-finished job. Sequencing across the boundary is expressed by where the trigger job sits in the parent's stages.
  • When is a dynamic child pipeline the wrong answer to a variable job set?
    When the variation is bounded and knowable in advance. A fixed set of components, a known matrix of platforms, or a handful of conditional jobs are all expressible with `rules:` and parallel jobs in one pipeline, where they stay reviewable. Generating YAML trades reviewability for flexibility, and it is only worth it when the flexibility is genuinely needed.

saying these in an interview costs you the question

  • Omits strategy: depend and calls a green parent a passing build
  • Expects needs: to order jobs across pipelines
  • Assumes a child inherits every parent variable automatically
  • Splits purely because the YAML file is long
  • Treats generated pipeline YAML as reviewed code

context