You wrote a beautiful set of dropdowns for your pipeline. You opened Run new pipeline. The form was empty. What happened?

The Setup

You have a parent .gitlab-ci.yml that delegates to a child via trigger:include::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# .gitlab-ci.yml (parent)
stages:
  - trigger

artifact-sync:
  stage: trigger
  trigger:
    include: .gitlab/pipelines/artifact-sync.yml
    strategy: depend
  rules:
    - if: $CI_PIPELINE_SOURCE == "web"

And the child file declares a rich variable schema with options and descriptions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# .gitlab/pipelines/artifact-sync.yml (child)
variables:
  SYNC_GRAFANA:
    value: "false"
    options: ["true", "false"]
    description: "Sync Grafana (chart + images)"
  SYNC_LOKI:
    value: "false"
    options: ["true", "false"]
    description: "Sync Loki (chart + images)"
  # ...eight more

You expect ten dropdowns in the Run new pipeline form. You get an empty Variables section and a free-form key/value row.

Why It Happens

GitLab’s Run new pipeline form is rendered before any pipeline exists. At form-render time, GitLab parses one thing: the project’s CI config (.gitlab-ci.yml by default, or whatever CI_CONFIG_PATH points to). Top-level include: directives are followed recursively, because they’re part of the same configuration compilation.

trigger:include: is not followed. It’s a runtime construct, not a config-time one.

When you write trigger:include: .gitlab/pipelines/artifact-sync.yml, you’re telling GitLab: “At runtime, when this job runs, create a brand-new downstream pipeline from that file.” The downstream pipeline is a separate object with its own ID, its own variables, its own jobs. From the parent’s perspective, the child path is just a string in the trigger config — not a file to load and merge.

So the parser stops at the trigger boundary. Anything inside the child file — stages, variables, workflow, even nested include: — is parsed only when the trigger job actually fires. By then the form has long since rendered and disappeared.

The Mental Model

Mechanism When parsed Variables visible in form?
Top-level include: Config compile (form-render time)
trigger: include: (child pipeline) Runtime, after parent job starts
trigger: project: (multi-project) Runtime, on a different project

Same logic: each pipeline has its own compilation scope. The form sees only the parent’s compiled config.

The Fix

Lift the variables to the root .gitlab-ci.yml. They render in the form, and they’re inherited into the child pipeline’s scope by default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# .gitlab-ci.yml (parent)
stages:
  - trigger

variables:
  SYNC_GRAFANA:
    value: "false"
    options: ["true", "false"]
    description: "Sync Grafana (chart + images)"
  # ... rest at root ...

artifact-sync:
  stage: trigger
  trigger:
    include: .gitlab/pipelines/artifact-sync.yml
    strategy: depend
  rules:
    - if: $CI_PIPELINE_SOURCE == "web"

The dropdowns now render. The child pipeline still sees SYNC_GRAFANA because parent → child variable inheritance is on by default for trigger jobs.

Watch Out For Sibling Pollution

If you have multiple trigger jobs in the parent (say, artifact-sync and repo-mirror), the lifted variables now leak into every child pipeline. Often harmless — they’re just unused env vars — but if you care about scope hygiene or have variable name collisions, opt the unrelated trigger out:

1
2
3
4
5
6
7
8
9
repo-mirror:
  stage: trigger
  inherit:
    variables: false              # ← block parent variables here
  trigger:
    include: .gitlab/pipelines/repo-mirror.yml
    strategy: depend
  rules:
    - if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

inherit: variables: false blocks all root-level variables for that job. If the job needs a specific one, switch to the allowlist form: inherit: variables: [VAR_A, VAR_B].

A Subtler Corollary

Even at runtime, variables in the parent’s job env aren’t auto-pushed to the child unless the trigger forwards them. Manually-set UI variables forward by default; some scheduled-pipeline cases don’t. Variable propagation across the parent → child boundary is opt-in, by design — same reason as before: they’re separate pipeline objects.

This is also why spec:inputs defined in a child file doesn’t show up in the parent’s Inputs section either. The form sees one config; child inputs are scoped to the child compile.

Takeaway

The Run new pipeline form is rendered from the parent’s config compile. trigger:include: is a runtime instruction, not part of that compile. If you want a variable to appear in the form, define it where the form is parsed — the project’s root CI config. Anything behind a trigger boundary is invisible to the UI by construction.

When the dropdowns are missing, don’t reach for the docs on value/options/description syntax — reach for git grep "trigger:" .gitlab-ci.yml.