skip to content

In a GitLab CI job's `rules:` list, how does GitLab decide whether the job is added to the pipeline, and what does a rule with `when: never` do?

level: middleimportance: must knowfreq 80%

answer

  1. evaluated when the pipeline is created
  2. order matters, evaluation stops early
  3. one winner supplies the attributes
  4. no match has the same effect as never
  5. default when is on_success

basics

~20 s

GitLab evaluates a job's rules top to bottom when the pipeline is created and stops at the first rule that matches, using that rule's when. A matching rule with when: never drops the job, and if no rule matches the job is never created.

solid answer

~40 s

`rules:` is evaluated at **pipeline creation time**, not while the job runs. GitLab walks the list in order, evaluates each rule's `if`, `changes` and `exists` conditions, and stops at the **first** rule that matches. That rule alone supplies the job's attributes — `when`, and optionally `allow_failure`, `variables`, `needs` or `timeout`. If the matching rule says `when: never`, the job is not added to the pipeline at all; there is no skipped placeholder to retry. If the rule omits `when`, the default is `on_success`. If **no** rule matches, the job is also not added — which is why an exclusion rule must come before the broad catch-all, and why a config whose last rule is `when: never` produces jobs only for the earlier cases. `rules:` cannot be combined with `only`/`except` in the same job.

code

yaml · 12 lines
yaml
deploy:
  stage: deploy
  script: ./deploy.sh production
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: never
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: on_success
    - when: manual
      allow_failure: false

go deeper

for a junior

Be able to read a rules block aloud and say which branch or event makes the job appear. Know that rules replaced only/except and that the common conditions are if, changes and exists.

for a middle

Explain first-match-wins and that only the winning rule supplies when, allow_failure and variables. Say plainly that no match and when: never both mean the job is not created, and that evaluation happens at pipeline creation.

for a senior

Show judgment about ordering: allowlist versus denylist styles, why an exclusion must precede the catch-all, and how you verify a rules change before it reaches shared branches rather than debugging it in production pipelines.

for a principal

Own the convention across many repositories: whether teams write their own rules or inherit a standard guard, how you keep rule complexity from becoming unreviewable, and the cost of rules that silently produce empty pipelines.

## What `rules:` is for Every job in a `.gitlab-ci.yml` is a candidate, not a certainty. When a pipeline is created, GitLab decides for each job whether that job exists in this pipeline and, if so, under what conditions it will run. `rules:` is the expression language that makes that decision. It superseded the older `only`/`except` keywords, which could not express "this OR that, unless the other thing" and could not attach variables or `allow_failure` to a condition. The crucial mental model: **rules are evaluated once, at pipeline creation, before any job starts.** Nothing a job does at runtime can bring a job into the pipeline that the rules excluded. A job whose rules did not match is not "skipped" — it does not appear in the pipeline graph and cannot be retried into existence. ## First match wins GitLab reads the list top to bottom. For each entry it evaluates the clauses present (`if`, `changes`, `exists`), which are ANDed together. The first entry whose clauses all evaluate true is the winner, evaluation stops, and **only that entry** contributes attributes to the job. Later entries are never consulted, even if they would also have matched. ```yaml deploy: script: ./deploy.sh rules: - if: $CI_COMMIT_BRANCH != "main" when: never - if: $CI_PIPELINE_SOURCE == "schedule" when: never - if: $CI_COMMIT_BRANCH == "main" when: manual allow_failure: false ``` Read that as a filter chain: everything not on `main` is excluded, scheduled runs are excluded, and what survives becomes a manual job. Reorder the entries and the meaning changes completely — putting the `main` rule first would let scheduled pipelines on `main` create the job. ## The three condition clauses - `if:` — a CI/CD variable expression. It supports `==`, `!=`, the regex operators `=~` and `!~`, `&&`, `||`, parentheses, and a bare variable name as a presence/non-empty test. Variables are the predefined `CI_*` set plus your own. Common ones: `$CI_COMMIT_BRANCH`, `$CI_DEFAULT_BRANCH`, `$CI_PIPELINE_SOURCE`, `$CI_COMMIT_TAG`, `$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`. - `changes:` — matches when the listed paths were modified. It has real caveats about *what* it compares against, depending on the pipeline type. - `exists:` — matches when a file matching the glob is present in the repository at that commit. Useful for "build a Docker image only if this component has a `Dockerfile`". ## What a matching rule contributes The winning rule supplies the job's `when`, which is `on_success` if the rule omits it. Values are `on_success`, `on_failure`, `always`, `manual`, `delayed` (with `start_in`), and `never`. A rule may also set `allow_failure`, job-level `variables`, `needs`, `timeout` and `interruptible`, which is how one job definition serves several contexts: ```yaml test: script: ./test.sh $SUITE rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" variables: SUITE: fast - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH variables: SUITE: full ``` ## `when: never` and the no-match case These two produce the same outcome — no job — but they read very differently in review. `when: never` is an explicit exclusion that stops evaluation; a fall-through with no match is an implicit exclusion. Both are legitimate styles: an *allowlist* config lists only the cases that should run and relies on no-match to exclude everything else, while a *denylist* config excludes the unwanted cases and ends with a broad `- if: $CI_COMMIT_BRANCH` catch-all. Mixing the two carelessly is the usual source of "why did nothing run?" A related trap: `when: never` at the **job** level excludes one job, while `when: never` inside `workflow:rules` prevents the **whole pipeline** from being created. ## Gotchas worth naming in an interview - You cannot use `rules` and `only`/`except` in the same job; GitLab rejects the config. - `rules` decide *existence*, not runtime behaviour. "Run this job only if the previous one failed" is `when: on_failure`, not a rule. - Variables that only exist at runtime (values a script computes) cannot drive `rules:if`, because the expression is evaluated before any job runs. - Regex in `if:` uses `=~ /pattern/` syntax without quotes; quoting it turns the comparison into a string equality test that silently never matches. - Use the pipeline editor's lint/simulation to check a config before pushing; a rules mistake is invisible until a pipeline is created.

  • If a job's rules match a `when: manual` entry, does the pipeline wait for someone to click it?
    Not by default. A manual job is created in a waiting state, but the pipeline continues past it and can finish as successful, because manual jobs are `allow_failure: true` by default in `rules`. Set `allow_failure: false` on that rule to make the pipeline block on the manual gate instead.
  • Why can't a variable set by an earlier job's script drive a later job's `rules:if`?
    Because rules are evaluated once, at pipeline creation, before any job runs. At that point only predefined, project, group and file-level variables exist. To vary behaviour on a computed value you either pass it through `dotenv` artifacts into the job's environment and branch inside the script, or generate a child pipeline whose YAML encodes the decision.
  • How would you express "run on merge requests and on the default branch, but never on scheduled pipelines"?
    Put the exclusion first: `- if: $CI_PIPELINE_SOURCE == "schedule"` with `when: never`, then `- if: $CI_PIPELINE_SOURCE == "merge_request_event"`, then `- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH`. First-match-wins makes the leading exclusion authoritative; the two positive rules default to `when: on_success`.

saying these in an interview costs you the question

  • Thinks all matching rules apply, not just the first
  • Says rules are evaluated while the job runs
  • Believes a non-matching job appears as skipped and can be retried
  • Mixes rules with only/except in one job
  • Assumes omitting when means the job always runs

context