Docs & Code Guide

GitLab CI pipeline diagram: resolve the includes, diagram the needs DAG

The .gitlab-ci.yml at the repo root is an entry point, not a pipeline. include pulls in local files, templates, components, and config from other projects, and what actually runs is the merge of all of them. Diagram the entry file and you draw a fraction of the truth; resolve the merge first and the whole DAG is on the table, ready to redraw every time the config moves.

7 min readFor teams whose GitLab pipeline config spans more include files than anyone remembers

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

195/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Resolve the includes first

GitLab will do the merge for you: the CI Lint API validates the configuration in the context of the project and returns the fully merged YAML, includes resolved, in the merged_yaml field of the response. The pipeline editor shows the same thing interactively in its full configuration tab. Either way, the merged document is the diagram input, because it is the only artifact in which a job contributed by a template three includes deep actually appears.

# The merged, effective config: includes resolved by GitLab itself
curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "$CI_API_V4_URL/projects/$CI_PROJECT_ID/ci/lint" \
  | jq -r '.merged_yaml'

needs is the DAG, stages are just columns

Without needs, GitLab runs stages as a strict sequence: every job in test waits for all of build, whether it depends on it or not. needs breaks that: a job with needs: [compile] starts the moment compile finishes, regardless of stage, and a job with needs: [] starts immediately at pipeline creation. The real execution graph is the needs DAG, and the stages are just the columns it is drawn over.

That split is exactly how to draw it: stages as background columns, needs as explicit arrows cutting across them. The gaps become visible in a way YAML never shows: the job still waiting on an entire stage when it needs one artifact, the needs: [] utility job nobody realizes runs first, the fan-in job that serializes an otherwise parallel pipeline.

Child and multi-project pipelines are subgraphs

trigger with include spawns a child pipeline inside the same project, and the config can even be generated by an earlier job and passed as an artifact, a dynamic child pipeline whose shape is decided at run time. trigger with project starts a pipeline in a different project entirely. Both belong on the map as subgraphs with a trigger edge in: collapsed to a single node when the reader only needs to know the handoff exists, expanded when the child is the pipeline being explained.

Dynamic children deserve an honest annotation: their merged config does not exist until a run generates it, so the standing diagram shows the generator job and a representative child, not a guaranteed shape.

A pipeline job that refreshes the diagram

The refresh belongs in the pipeline it documents: a job guarded by rules with changes on the CI config paths runs only when the pipeline definition itself changes, pulls the merged YAML from the Lint API, and hands it to an agent connected to Datadef's MCP server, registry name io.datadef/mcp. Stated plainly: Datadef does not watch the repository, and GitLab does not call Datadef; the rules:changes guard is the detection and the agent invocation is the one command, authenticated by an API key kept in CI variables, available on paid plans.

First-time generation is a different job than refreshing: paste the merged YAML or a description into the DevOps pipeline diagram generator and shape the initial map there, then let the pipeline own every update after.

refresh-pipeline-diagram:
  stage: .post
  rules:
    - changes:
        paths:
          - ".gitlab-ci.yml"
          - "ci/**/*.yml"
  script:
    - MERGED=$(curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
      "$CI_API_V4_URL/projects/$CI_PROJECT_ID/ci/lint" | jq -r '.merged_yaml')
    - echo "$MERGED" > merged-ci.yml
    # Hand merged-ci.yml to your MCP-connected agent to update the diagram

Edges the merged view still hides

The merged YAML is the potential pipeline, not any single run: rules decide per pipeline which jobs exist, so a job gated on a schedule or a variable may never appear in the pipelines a given reader sees. The diagram should show the full graph and mark condition-gated jobs as such, rather than pretending every run looks the same.

And the standing constraints: embedding the live diagram in a GitLab wiki or README, covered in embedding diagrams in GitLab, requires the Datadef project shared public, and the loop keeps the pipeline map current, not the runbook prose around it.

FAQ

How do I see the full merged GitLab CI configuration with all includes resolved?

Call the project CI Lint API, which validates the config in project context and returns a merged_yaml field with every include resolved, or open the full configuration tab in the pipeline editor for the same view interactively. The merged document is the right input for any diagram or analysis, since included jobs never appear in the entry file.

What is the difference between stages and needs in a GitLab CI diagram?

Stages are a coarse sequence: without needs, each job waits for the entire previous stage. needs declares the true dependencies and lets jobs start the moment their inputs finish, with needs: [] starting immediately. Draw stages as columns and needs as arrows across them, and the difference between nominal order and real execution becomes visible.

How should child pipelines appear on the diagram?

As subgraphs with a trigger edge from the parent job: trigger plus include for a child in the same project, trigger plus project for a downstream project. Dynamic child pipelines, whose YAML is generated by an earlier job, should be annotated as run-time shaped, since their config does not exist until the pipeline runs.

Can the pipeline diagram refresh automatically when .gitlab-ci.yml changes?

Through the pipeline itself: a job guarded by rules:changes on the CI config paths runs only when the definition changes, fetches the merged YAML from the Lint API, and has an MCP-connected agent update the diagram. Nothing external watches the repo; the rules guard is the trigger.

Why does my diagram show jobs that never run in my pipelines?

Because the merged config is the set of possible jobs, and rules prune it per run: schedule-only jobs, variable-gated jobs, and branch-specific jobs all exist in the config while appearing only in some pipelines. Mark condition-gated jobs on the diagram instead of removing them; the conditions are part of the architecture.