Declarative Pipelines
Declarative Pipelines
Declarative Pipeline is the opinionated, structured DSL that Jenkins ships as the recommended way to define a CI/CD pipeline in code. It wraps Groovy in a strict grammar that makes pipelines readable, auditable, and maintainable at scale — exactly what big-tech teams need when a Jenkinsfile lives in every microservice repository and is reviewed like application code. This lesson dissects every first-class construct: pipeline, agent, stages/steps, post, and when.
The Mandatory Skeleton
A Declarative Pipeline must follow a precise top-level structure. Any deviation is a parse error at load time — which is actually a feature, because it surfaces misconfiguration before a build is ever triggered.
The parser enforces the grammar: you cannot place bare Groovy at the top level the way Scripted Pipeline allows. This constraint is the trade-off — you gain instant syntax feedback and tooling support (Blue Ocean, Replay), and you lose a small amount of raw flexibility that you rarely need anyway.
The agent Directive
The agent block answers one question: on which executor should this pipeline (or stage) run? Jenkins resolves the agent before executing any steps, so getting this right is foundational.
agent any— schedule on any available executor. Fine for small setups; not recommended in production where you want reproducible build environments.agent none— no top-level agent; eachstagemust declare its own. This is the production pattern: different stages run on different agents (e.g., build on a Docker image, deploy on a privileged agent with cloud credentials).agent { label 'linux && docker' }— schedule on any node whose tags match the label expression. Labels are how you route stages to the right hardware or OS.agent { docker { image 'node:20-alpine' } }— spin up a fresh Docker container per stage; the workspace is mounted inside automatically. This is the recommended pattern for build stages in modern Jenkins installs.agent { kubernetes { yaml '...' } }— provision a Pod in Kubernetes as the executor. The Jenkins Kubernetes Plugin makes this the de-facto standard at cloud-native companies.
agent none + per-stage agents: declare agent none at the top level and assign explicit agents per stage. This prevents the controller from holding an executor slot idle while slow stages (integration tests, publishing) run.
Stages and Steps
Every meaningful unit of work lives inside a stage. Stages are the granularity level that Blue Ocean visualises and that your team reasons about: Build, Unit Test, Integration Test, Security Scan, Publish, Deploy. Within a stage, steps is the block where you call step functions.
Key built-in steps you will use constantly:
sh 'command'— run a shell command on Linux/macOS agents.bat 'command'— run a batch command on Windows agents.echo 'message'— print to build log.checkout scm— check out the source that triggered this build (implicit in Multibranch, explicit otherwise).stash/unstash— pass artifacts between stages that run on different agents.withCredentials([...])— inject secrets into the environment for the duration of the block.dir('path')— change working directory for nested steps.timeout(time: 10, unit: 'MINUTES')— fail a step or stage if it hangs.
agent and when; post runs after all stages complete.The post Section
The post block defines actions that execute after the pipeline (or individual stage) finishes, regardless of outcome. It can appear at the top level and inside any stage. The available conditions map to every meaningful final state:
always— unconditional; use for cleanup and log archival.success— only when the build passes; use for publishing artifacts or notifications.failure— only on failure; use for alerting on-call, rolling back.unstable— build completed but test failures set the result to UNSTABLE.changed— fires when the current result differs from the previous run; excellent for "back to green" Slack messages.fixed— shorthand for success-after-failure.regression— shorthand for failure-after-success.aborted— the build was manually stopped.cleanup— runs last of all, after every other post condition; use for workspace cleanup so it always happens even if a notification step fails.
deleteDir() or container teardown in cleanup rather than always. If a Slack notification throws, always would abort before reaching cleanup; cleanup is guaranteed to run after everything else in post.
The when Directive
The when block lets you skip a stage entirely based on conditions evaluated before the stage's agent is allocated. This is critical for efficient pipelines: why spin up a deployment container on every feature branch commit when you only deploy from main?
Common built-in conditions:
branch 'main'— matches the branch name (glob supported:'release/*').tag 'v*'— matches a Git tag.environment name: 'DEPLOY_ENV', value: 'production'— inspects an environment variable.expression { return params.RUN_INTEGRATION_TESTS }— arbitrary Groovy expression returning a boolean.changeRequest()— true when the build is a Pull Request.not { branch 'main' }— negation.allOf { branch 'main'; environment name: 'CI', value: 'true' }— logical AND.anyOf { branch 'main'; branch 'staging' }— logical OR.
Environment Variables and Options
Two more first-class directives complete the declarative grammar. The environment block injects variables into env for the pipeline or stage scope. The options block configures pipeline-level behaviors:
timeout(time: 30, unit: 'MINUTES')— kill the whole pipeline if it exceeds the budget.retry(3)— retry the entire pipeline on failure (use per-stage retry for finer control).buildDiscarder(logRotator(numToKeepStr: '10'))— prevent disk exhaustion by capping retained builds.disableConcurrentBuilds()— ensures only one instance of this pipeline runs at a time; critical for deploy pipelines.skipDefaultCheckout()— disables the automatic source checkout so you control it explicitly withcheckout scm.
buildDiscarder: a high-velocity service with no log rotation will fill the Jenkins controller disk within days. Every Jenkinsfile should define buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '5')) as a baseline.
Common Failure Modes
Understanding how the grammar fails is as important as knowing how to use it:
- Parse-time errors: Groovy syntax mistakes, undeclared variables, and invalid directive placements are caught when Jenkins loads the Jenkinsfile — before any executor is allocated. Always use the Replay feature to iterate quickly without committing.
- Agent allocation timeout: if no matching agent is available, Jenkins will queue the build indefinitely unless you set
options { timeout(...) }. whenevaluated before agent: by defaultwhenis evaluated before the stage agent is allocated — good for efficiency. If you need the agent already running (e.g., to inspect the workspace), addbeforeAgent falseinside thewhenblock.- stash/unstash across agents: stash is stored on the controller; for large artifacts (hundreds of MB) it becomes a bottleneck. Use an external artifact store (Artifactory, S3, Nexus) and pass only coordinates between stages.
Declarative Pipeline's strict grammar is not a limitation — it is the contract that makes Jenkinsfiles auditable, diff-able, and maintainable across hundreds of repositories. Master this grammar before reaching for Scripted Pipeline's escape hatches.