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?
answer
- evaluated when the pipeline is created
- order matters, evaluation stops early
- one winner supplies the attributes
- no match has the same effect as never
- default when is on_success
basics
~20 sGitLab 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 linesdeploy:
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: falsego deeper
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.
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.
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.
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