Stop Running Every Stage on Every Branch: Jenkins Declarative Pipeline's when Directive
Jenkins Declarative Pipeline's when directive skips stages before they consume executors. Learn the built-in conditions, a worked deploy-gating example, and the trade-offs.
02 Sept 2026, 23:18 UTC

Your pipeline has a deploy stage, a performance-test stage, and a security-scan stage. Right now they all run on every push to every branch — including that typo fix on feature/readme. Executors get tied up, feedback slows down, and your deploy step is one bad if block away from running somewhere it shouldn't.
The fix isn't more scripting inside your steps. It's the Declarative Pipeline when directive, which lets Jenkins decide before a stage starts whether it should run at all. Skipped stages show up as SKIPPED in the stage view, never allocate an executor, and keep your pipeline's intent readable at a glance.
What when actually does
In a Declarative Pipeline (requires the Pipeline: Declarative plugin, version 1.2 or later — effectively universal on any maintained Jenkins), each stage can carry a when block containing one or more conditions. Jenkins evaluates those conditions after the agent and options are resolved but before any step executes. If the condition is false, the stage is skipped cleanly: no executor time, no partial side effects, no fake "success" from an if that silently did nothing.
That last point matters. A common pattern is guarding a deploy with Groovy logic inside steps:
stage('Deploy') {
steps {
script {
if (env.BRANCH_NAME == 'main') {
sh './deploy.sh'
}
}
}
}This works, but the stage still claims an executor, shows green in the UI even when it did nothing, and buries the gating logic inside imperative code. when moves that decision up front, where it belongs.
The conditions you'll actually use
The built-in conditions cover most real gating needs:
branch 'main'— run only on a matching branch (supports Ant-style patterns likerelease/*). Only meaningful in multibranch pipelines.environment name: 'DEPLOY', value: 'yes'— run when an environment variable or parameter has a given value.expression { ... }— arbitrary Groovy returning a boolean, for anything the built-ins don't cover.changeset 'src/**'— run only when the commit touched matching paths. Useful for skipping docs-only changes, though it depends on SCM changelog data being available and accurate.- Combinators:
allOf,anyOf, andnotlet you compose conditions declaratively.
A worked example: gated deploy with a manual override
Here's a realistic pipeline: tests run everywhere, deploy runs only on main, and a hotfix deploy can be forced from any branch via a build parameter:
pipeline {
agent any
parameters {
booleanParam(name: 'FORCE_DEPLOY', defaultValue: false,
description: 'Deploy even off main')
}
stages {
stage('Test') {
steps { sh './run-tests.sh' }
}
stage('Deploy to prod') {
when {
anyOf {
branch 'main'
expression { params.FORCE_DEPLOY == true }
}
}
steps { sh './deploy.sh production' }
}
stage('Performance tests') {
when {
allOf {
branch 'main'
changeset 'src/**'
}
}
steps { sh './perf-suite.sh' }
}
}
}To verify the behavior yourself: in a multibranch pipeline, push a commit to a non-matching branch and check the stage view — the deploy stage should render as SKIPPED (greyed out, not green). Then push to main (or build with FORCE_DEPLOY checked) and confirm the stage executes and its steps appear in the log. For the changeset stage, push a commit that only edits a file outside src/ and confirm it's skipped, then touch a file under src/ and confirm it runs.
Trade-offs and limitations
when is not a universal gate. A few things to keep in mind:
- Readability cuts both ways. A deeply nested
allOf/anyOf/notstack can be harder to reason about than a single well-namedexpression. If a condition grows past two levels of nesting, extract it into a shared library function and call it fromexpression. - changeset depends on SCM data. On first builds of a branch, shallow clones, or force-pushed history, the changelog may be empty or misleading. Treat
changesetas an optimization, not a correctness guarantee — don't use it to skip security-critical stages. - Skipped is not failed. If your deploy stage is skipped because the branch didn't match, the build is still green. If you need "deploy must happen or the build fails," that logic belongs in an explicit post-build check, not in
when. - Agent allocation nuance. By default,
whenis evaluated before the stage's agent is allocated (with a top-level agent already running). If your condition needs files from the workspace, look at thebeforeAgent trueoption carefully — the default behavior is usually what you want, but workspace-dependent conditions change the picture.
Where to start
Pick the one stage in your pipeline that most obviously shouldn't run everywhere — usually deploy or a slow test suite — and wrap it in a when { branch 'main' }. Run it once on a feature branch and once on main, and confirm the SKIPPED/executed behavior in the stage view. That ten-minute change typically pays for itself in executor time within a week, and it makes your Jenkinsfile read like a description of intent instead of a pile of defensive if statements.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.