| | |

GitLab 13 🦊 Controlling Job Execution with rules, only, and except

⏱️ Estimated read: 10 min | |

A pipeline that runs the same jobs on every branch, every commit, and every tag is a pipeline that wastes resources and delays feedback. The rules, only, and except keywords control which jobs run and when. The LFCS exam does not test GitLab, but the GitLab Certified Associate exam does. The critical distinction is that rules is the modern mechanism, and only/except are the legacy mechanism. Both are still supported, but rules is more expressive and is the recommended approach for new pipelines. A job with a rules clause evaluates conditions in order, and the first match determines whether the job runs, is delayed, or is excluded. A job without rules runs by default.

Key point: rules replaces only and except. A rules list is evaluated top to bottom. The first rule with a matching if condition (or no if, which matches everything) determines the job’s behavior. If no rule matches, the job does not run. The when keyword within a rule controls the outcome: on_success, on_failure, always, never, manual, or delayed. The only and except keywords still work but are less flexible.


Why execution control matters

The resource problem. Every job consumes runner minutes. A pipeline that runs deployment jobs on every feature branch, or integration tests on every documentation change, wastes money and time. The rules keyword lets the pipeline run the correct jobs for the correct context. A deploy job runs only on the main branch. An integration test runs only when the source code changes, not when the README changes .

The feedback problem. A merge request pipeline should run the tests that validate the proposed change. A scheduled pipeline should run the full suite, including the jobs that are too slow for every commit. The rules conditions can check the pipeline source ($CI_PIPELINE_SOURCE), the branch name ($CI_COMMIT_BRANCH), the tag ($CI_COMMIT_TAG), the file changes ($CI_COMMIT_BEFORE_SHA with changes), and the merge request target ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME). This allows the pipeline to behave differently depending on why it was triggered.

The duplicate pipeline problem. A project with both branch pipelines and merge request pipelines can create two pipelines for the same commit: one for the push to the branch, and one for the merge request event. The workflow:rules keyword controls whether the pipeline is created at all. A common pattern is to skip branch pipelines when a merge request exists, which eliminates the duplicate and reduces the load on the runners .

The legacy problem. The only and except keywords were the original mechanism. They accept branch names, tags, and special keywords like merge_requests, branches, tags. They are still supported, but they cannot express complex conditions like “run if the branch is main AND the pipeline source is a schedule.” The rules keyword can, and it is the recommended approach for new pipelines.

The trade-off. rules is more complex to read than only/except, but it is also more precise. A rules list that is well-structured is easier to maintain than a combination of only and except that are scattered across jobs. The exam tests both, but the practical recommendation is rules.


a. The rules keyword: conditions and outcomes

The rules keyword is a list of rules. Each rule can have an if condition and a when outcome. The rules are evaluated in order. The first match determines the job’s behavior.

deploy:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success
    - if: $CI_COMMIT_TAG
      when: manual
    - when: never

The deploy job runs on main automatically, on a tag it runs manually, and on any other branch it does not run. The final when: never is the fallback: without it, a job with no matching rule is excluded by default, but the explicit fallback makes the intent clear.

The if condition can use any of the predefined variables. The most common are $CI_COMMIT_BRANCH, $CI_COMMIT_TAG, $CI_PIPELINE_SOURCE, and $CI_MERGE_REQUEST_TARGET_BRANCH_NAME.

test:
  script: npm test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

The test job runs on merge request pipelines and on the main branch. It does not run on other branches.

The changes keyword within a rule matches when specific files have changed. This is the mechanism for running a job only when the relevant files are modified.

docs:
  script: make docs
  rules:
    - changes:
        - docs/**/*

The docs job runs only when a file in the docs/ directory is modified. The pattern is a glob. The changes keyword works only in branch pipelines and merge request pipelines. It does not work in scheduled pipelines, because there is no previous commit to compare against .


b. The when outcomes

The when keyword within a rule determines what happens when the rule matches. The default is on_success, which means the job runs if the previous stages succeed. The other values are on_failure, always, never, manual, and delayed.

cleanup:
  script: ./cleanup.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: always

The cleanup job runs regardless of whether the previous jobs succeeded or failed. This is the correct setting for cleanup jobs that must run even when the build fails.

The when: manual setting makes the job require a manual trigger. The job appears in the pipeline but does not run until a user clicks the play button.

deploy:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual

The deploy job is created on the main branch but does not run automatically. This is the mechanism for a production deployment that requires an approval.

The when: never setting excludes the job entirely. It does not appear in the pipeline.


c. The only and except keywords

The only and except keywords are the legacy mechanism. The only keyword specifies when the job runs. The except keyword specifies when it does not.

test:
  script: npm test
  only:
    - main
    - merge_requests
  except:
    - schedules

The test job runs on the main branch and on merge requests, but not on scheduled pipelines. The only and except accept branch names, tags, and special keywords: branches, tags, merge_requests, schedules, pushes, web, api.

The only:variables syntax allows variable-based conditions. The only:changes syntax allows file-change conditions. These are the legacy equivalents of the rules conditions, but they are less flexible and are not recommended for new pipelines.

test:
  only:
    variables:
      - $CI_COMMIT_BRANCH == "main"

The only and except keywords are deprecated but still supported. They are slower than rules because they require a separate evaluation pass, and they cannot express the complex conditions that rules can. The exam tests them because they appear in existing pipelines, but the correct approach for new pipelines is rules.


Complete Example Session

# ============================================
# PART 1: BASIC RULES
# ============================================

deploy:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
# ============================================
# PART 2: RULES WITH FALLBACK
# ============================================

deploy:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success
    - when: never
# ============================================
# PART 3: MERGE REQUEST RULES
# ============================================

test:
  script: npm test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
# ============================================
# PART 4: FILE-CHANGE RULES
# ============================================

docs:
  script: make docs
  rules:
    - changes:
        - docs/**/*
# ============================================
# PART 5: MANUAL DEPLOY
# ============================================

deploy:
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
# ============================================
# PART 6: ALWAYS RUN CLEANUP
# ============================================

cleanup:
  script: ./cleanup.sh
  rules:
    - when: always
# ============================================
# PART 7: LEGACY ONLY/EXCEPT
# ============================================

test:
  script: npm test
  only:
    - main
    - merge_requests
  except:
    - schedules
# ============================================
# PART 8: WORKFLOW RULES
# ============================================

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
    - when: never
# ============================================
# PART 9: VERIFY IN PIPELINE EDITOR
# ============================================

# Build → Pipeline editor → Lint
# Shows whether each job will run.

The nine parts trace the rules workflow: basic rules, fallback, merge request rules, file-change rules, manual deploy, always cleanup, legacy only/except, workflow rules, and editor validation.


Quick Reference

rules Keywords

KeywordPurpose
ifCondition
whenOutcome
changesFile change pattern
existsFile existence pattern
allow_failureAllow failure

when Values

ValueMeaning
on_successRun if previous stages pass
on_failureRun only on failure
alwaysAlways run
neverNever run
manualManual trigger
delayedDelayed start

Common Variables

VariableValue
$CI_COMMIT_BRANCHBranch name
$CI_COMMIT_TAGTag name
$CI_PIPELINE_SOURCETrigger source
$CI_MERGE_REQUEST_TARGET_BRANCH_NAMEMR target

Pipeline Sources

SourceMeaning
pushBranch push
merge_request_eventMerge request
scheduleScheduled
webManual trigger
apiAPI trigger

Legacy Keywords

KeywordPurpose
onlyWhen to run
exceptWhen not to run
only:variablesVariable-based condition
only:changesFile-change condition

Best Practices

✅ Do This:

rules:
  - if: $CI_COMMIT_BRANCH == "main"
    when: manual
  • ✅ Use rules for new pipelines
rules:
  - changes:
      - docs/**/*
  • ✅ Run jobs only when relevant files change
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
    - when: never
  • ✅ Control pipeline creation with workflow:rules

❌ Don’t Do This:

# Mix rules with only/except
<!-- Conflicting behavior -->                        # ❌
# Omit the fallback when: never
<!-- Jobs run by default when no rule matches -->    # ❌
# Use `changes` in scheduled pipelines
<!-- There is no previous commit to compare -->      # ❌

Common Pitfalls

PitfallWhy It HappensFix
Job runs on every branchNo rules or onlyAdd rules
Job excluded unexpectedlyNo matching ruleAdd fallback or fix condition
Duplicate pipelinesBranch + MR pipelinesUse workflow:rules
changes ignoredScheduled pipelineUse only:changes or remove
Job not running on MRNo merge_request_event ruleAdd condition

Real-World Examples

1. Main Branch Only

rules:
  - if: $CI_COMMIT_BRANCH == "main"

2. Merge Request

rules:
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"

3. Tag Only

rules:
  - if: $CI_COMMIT_TAG

4. Manual

rules:
  - if: $CI_COMMIT_BRANCH == "main"
    when: manual

5. Always

rules:
  - when: always

6. File Changes

rules:
  - changes:
      - src/**/*

7. Legacy Only

only:
  - main
  - merge_requests

8. Legacy Except

except:
  - schedules

9. Workflow Rules

workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - when: never

10. Complex Condition

rules:
  - if: $CI_COMMIT_BRANCH == "main"
    when: manual
    allow_failure: false

Visual

Rule Evaluation Flow

┌──────────────────────────────────────────────┐
│  Rule 1: if: $CI_COMMIT_BRANCH == "main"     │
│  ├─ Match → job runs                         │
│  └─ No match → continue                      │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Rule 2: if: $CI_COMMIT_TAG                  │
│  ├─ Match → job runs manually                │
│  └─ No match → continue                      │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Rule 3: when: never                         │
│  → job excluded                              │
└──────────────────────────────────────────────┘

Workflow Rules

┌──────────────────────────────────────────────┐
│  Pipeline trigger                            │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  workflow:rules                              │
│  ├─ MR event → create pipeline               │
│  ├─ main branch → create pipeline            │
│  └─ other → no pipeline                      │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Pipeline created or rejected                │
└──────────────────────────────────────────────┘

Summary

ItemValue
Modern keywordrules
Legacy keywordsonly, except
when valueson_success, on_failure, always, never, manual, delayed
File change conditionchanges
Pipeline creation controlworkflow:rules
Common variables$CI_COMMIT_BRANCH, $CI_PIPELINE_SOURCE, $CI_COMMIT_TAG

Key takeaways:

  • rules is the modern mechanism. It evaluates conditions in order and determines whether the job runs, is delayed, or is excluded. The when: never fallback is the standard pattern .
  • only and except are legacy. They are still supported but less flexible and slower. The exam tests them because they appear in existing pipelines.
  • The workflow:rules keyword controls pipeline creation. It is the mechanism for preventing duplicate pipelines when both branch and merge request pipelines are enabled .
  • The changes keyword runs jobs only when relevant files are modified. It works only in branch pipelines and merge request pipelines. It does not work in scheduled pipelines.
  • The when: manual outcome creates a manual gate. The job is created but does not run until a user triggers it. This is the mechanism for production deployment approvals.

Remember: The rules keyword is the gate. The if condition is the check. The when outcome is the decision. The first matching rule wins. The fallback (when: never) is the safety net. The workflow:rules is the pipeline-level gate. The only/except is the legacy path. The exam tests the conditions and the outcomes. The practice is the only way to internalize the precedence.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!