GitLab’s parent-child pipelines are actually a way to hide complexity, not just split it.

Let’s watch a typical scenario. Imagine a monolithic application with a frontend, a backend, and some shared libraries. Building and testing all of this sequentially in one giant .gitlab-ci.yml file is a nightmare. Dependencies get tangled, test suites take hours, and a small frontend change triggers a full backend rebuild.

Here’s a simplified gitlab-ci.yml for this monolith:

stages:
  - build
  - test
  - deploy

variables:
  DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA

build_backend:
  stage: build
  script:
    - echo "Building backend..."
    - docker build -t myapp/backend:$DOCKER_IMAGE_TAG ./backend
    - docker push myapp/backend:$DOCKER_IMAGE_TAG

build_frontend:
  stage: build
  script:
    - echo "Building frontend..."
    - docker build -t myapp/frontend:$DOCKER_IMAGE_TAG ./frontend
    - docker push myapp/frontend:$DOCKER_IMAGE_TAG

test_backend:
  stage: test
  needs: ["build_backend"]
  script:
    - echo "Testing backend..."
    - docker run myapp/backend:$DOCKER_IMAGE_TAG npm test

test_frontend:
  stage: test
  needs: ["build_frontend"]
  script:
    - echo "Testing frontend..."
    - docker run myapp/frontend:$DOCKER_IMAGE_TAG npm test

deploy_production:
  stage: deploy
  needs: ["test_backend", "test_frontend"]
  script:
    - echo "Deploying to production..."
    - # deployment logic here

This works, but it’s brittle. A change in build_backend might require updating test_backend’s script, and the whole pipeline grows with every new microservice or component.

Now, let’s split this using parent-child pipelines. The "parent" pipeline will orchestrate the "children."

Parent Pipeline (.gitlab-ci.yml):

stages:
  - build
  - test
  - deploy

variables:
  DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA

build_all_components:
  stage: build
  trigger:
    project: my-group/my-project # Or use 'strategy: depend' for same project
    branch: main
    file: .gitlab-ci/build-pipeline.yml # Path to child pipeline config

test_all_components:
  stage: test
  needs: ["build_all_components"]
  trigger:
    project: my-group/my-project
    branch: main
    file: .gitlab-ci/test-pipeline.yml

deploy_production:
  stage: deploy
  needs: ["test_all_components"]
  script:
    - echo "Deploying to production..."
    - # deployment logic here

Child Pipeline for Building (.gitlab-ci/build-pipeline.yml):

stages:
  - build

variables:
  DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA

build_backend_job:
  stage: build
  script:
    - echo "Building backend..."
    - docker build -t myapp/backend:$DOCKER_IMAGE_TAG ./backend
    - docker push myapp/backend:$DOCKER_IMAGE_TAG

build_frontend_job:
  stage: build
  script:
    - echo "Building frontend..."
    - docker build -t myapp/frontend:$DOCKER_IMAGE_TAG ./frontend
    - docker push myapp/frontend:$DOCKER_IMAGE_TAG

Child Pipeline for Testing (.gitlab-ci/test-pipeline.yml):

stages:
  - test

variables:
  DOCKER_IMAGE_TAG: $CI_COMMIT_SHORT_SHA

test_backend_job:
  stage: test
  needs: [] # Dependencies are handled by the parent pipeline's trigger
  script:
    - echo "Testing backend..."
    - docker run myapp/backend:$DOCKER_IMAGE_TAG npm test

test_frontend_job:
  stage: test
  needs: []
  script:
    - echo "Testing frontend..."
    - docker run myapp/frontend:$DOCKER_IMAGE_TAG npm test

In this setup, the parent pipeline’s trigger keyword is key. It tells GitLab to start a new pipeline based on the specified file and branch. The needs keyword on the parent’s trigger jobs ensures that the child pipelines run only after their predecessors in the parent’s stage complete.

The real power comes from strategy: depend. If your child pipelines are in the same project as the parent, you can omit project and branch from the trigger and use:

trigger:
  file: .gitlab-ci/build-pipeline.yml
  strategy: depend

This makes the parent pipeline’s jobs wait for the triggered child pipeline to complete successfully before proceeding. If any job in the child pipeline fails, the parent pipeline job that triggered it will also be marked as failed.

The parent-child relationship isn’t just about code organization; it’s a fundamental shift in how pipeline dependencies are managed. Instead of explicit needs between jobs in a single file, you’re now defining dependencies between pipelines. The parent pipeline acts as a scheduler, and each trigger job represents a dependency on a whole separate pipeline’s success.

The one thing most people don’t realize is that the needs relationship on the trigger job in the parent is not about the jobs within the child pipeline. It’s about ensuring the entire triggered child pipeline finishes before the parent moves to its next stage. If the child pipeline has multiple jobs, they all run concurrently (or as GitLab’s runner capacity allows), and the parent only cares about the overall success of that triggered pipeline.

This pattern is incredibly useful for monorepos, complex applications, or when you want to enforce distinct build, test, and deploy phases that can be managed and versioned independently.

The next hurdle you’ll face is managing complex variable passing and artifact sharing between these independent child pipelines.

Want structured learning?

Take the full Gitlab course →