Jenkins Course
Declarative vs Scripted Pipeline
There are two ways to write a Jenkinsfile. Every Jenkins engineer eventually encounters both. This lesson shows them side by side — what they look like, how they think, and why one became the industry standard.
This lesson covers
Declarative syntax → Scripted syntax → Side-by-side comparison → Key differences in structure → When each one is the right tool → Converting between them
In Lesson 11 you wrote your first Jenkinsfile using Declarative syntax — the structured, readable approach with pipeline { }, stages { }, and post { }. That's the modern standard. But Jenkins existed for years before Declarative syntax was introduced, and in that time teams wrote pipelines a completely different way — using Scripted syntax.
You need to understand both. Declarative because it's what you'll write. Scripted because you'll inevitably run into it in existing codebases, Stack Overflow answers, and legacy Jenkins setups — and you need to read it without being confused.
The Analogy
Declarative is like filling in a well-designed form — each field has a label, a purpose, and validation that catches mistakes before you submit. Scripted is like writing a letter in free prose — more expressive, more powerful, but with no guardrails. You can write anything. Including nonsense. The form catches more mistakes. The letter lets you say more.
The Same Pipeline — Written Two Ways
Here is the exact same three-stage pipeline written in both syntaxes. Read them side by side and notice the structural differences — same outcome, very different shape.
Declarative Pipeline
Introduced 2016 · Recommended today
Scripted Pipeline
Original syntax · Still valid
// DECLARATIVE PIPELINE
// Starts with the 'pipeline' keyword — always
pipeline {
// agent block is mandatory in Declarative
agent any
// All stages must live inside 'stages'
stages {
stage('Checkout') {
steps {
// steps block required inside every stage
checkout scm
}
}
stage('Test') {
steps {
sh './gradlew test'
}
}
stage('Deploy') {
steps {
sh './deploy.sh staging'
}
}
}
// post block handles success/failure actions
post {
success {
echo 'Deployed to staging successfully'
}
failure {
echo 'Pipeline failed — check console output'
}
}
}
// SCRIPTED PIPELINE
// Starts with 'node' — no 'pipeline' wrapper
node {
// No 'stages' container — stages are optional labels
// No mandatory structure — just Groovy code
stage('Checkout') {
// No 'steps' block needed — write code directly
checkout scm
}
stage('Test') {
sh './gradlew test'
}
stage('Deploy') {
sh './deploy.sh staging'
}
// No built-in 'post' block
// Handle success/failure with try/catch instead
// This runs after all stages complete
echo 'Deployed to staging successfully'
}
// Failure handling in Scripted uses try/catch
// You'd wrap the node block like this:
// try {
// node { ... }
// } catch (e) {
// echo "Failed: ${e}"
// throw e
// }
The Key Differences — Explained
| Feature | Declarative | Scripted |
|---|---|---|
| Starts with | pipeline { } |
node { } |
| Structure | Strict — must follow a defined schema. Jenkins validates it before running. | Freeform — write any valid Groovy code. No schema enforced. |
| Error catching | Jenkins catches many syntax mistakes before the build runs and shows a clear error. | Errors usually surface at runtime — the pipeline starts, then fails mid-run. |
| Post actions | Built-in post { } block with success, failure, always conditions. |
Manual — use Groovy try/catch/finally blocks. |
| Flexibility | High within the defined structure. Complex logic goes in a script { } block. |
Unlimited — it's pure Groovy. Any programming construct is available. |
| Readability | High — any engineer can read it without knowing Groovy. | Varies — readable if well-written, dense and complex if not. |
| Use today? | ✅ Yes — recommended for all new pipelines | ⚠️ Only for legacy code or complex edge cases |
The Escape Hatch — script { } in Declarative
Declarative has one trick that makes it more powerful than it first appears. Inside any steps block, you can open a script { } block and write pure Groovy code. This is the escape hatch — you get the clean structure of Declarative with the full power of Scripted where you need it.
The scenario:
You're a DevOps engineer at an e-commerce company. Your Declarative pipeline needs to make a decision mid-build — if the branch is main, deploy to production; if it's anything else, deploy to staging. That kind of conditional logic isn't available as a native Declarative directive, so you use a script { } block to write it in Groovy.
New terms in this code:
- script { } — a block inside a Declarative pipeline's
stepsthat lets you write free Groovy code. Use it when Declarative directives aren't flexible enough. - env.BRANCH_NAME — a built-in Jenkins environment variable that holds the name of the Git branch that triggered the build. Jenkins sets this automatically — you don't need to define it.
- Groovy if/else — standard programming conditional logic. In a
script { }block you can use any Groovy language feature — loops, conditionals, variable assignment, method calls.
// Declarative pipeline using a script{} block for conditional logic
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Test') {
steps {
sh './gradlew test'
}
}
stage('Deploy') {
steps {
// Open a script{} block to write Groovy logic
// This is the escape hatch from Declarative into Groovy
script {
// env.BRANCH_NAME is set automatically by Jenkins
// It contains the branch that triggered this build
if (env.BRANCH_NAME == 'main') {
// Deploy to production only when building main
sh './deploy.sh production'
echo 'Deployed to PRODUCTION'
} else {
// All other branches deploy to staging
sh './deploy.sh staging'
echo "Deployed branch ${env.BRANCH_NAME} to STAGING"
}
}
}
}
}
post {
success {
echo "Build and deploy complete for branch: ${env.BRANCH_NAME}"
}
failure {
echo 'Build failed — deployment skipped'
}
}
}
Where to practice: Add this Jenkinsfile to your repository from Lesson 11. Create a new branch called feature/test-branch, push it, and trigger the pipeline. You should see "Deployed to STAGING" in the output. Switch to main and trigger again — you should see "Deployed to PRODUCTION". Full environment variable reference at jenkins.io — Environment Variables.
Started by user admin
[Pipeline] Start of Pipeline
[Pipeline] node
Running on agent-linux-01 in /var/jenkins_home/workspace/checkout-service
[Pipeline] { (Checkout) }
[Pipeline] checkout
Cloning repository https://github.com/acmecorp/checkout-service.git
> git checkout feature/add-retry-logic
[Pipeline] { (Test) }
[Pipeline] sh
+ ./gradlew test
BUILD SUCCESSFUL in 48s
34 tests completed, 0 failed
[Pipeline] { (Deploy) }
[Pipeline] script
[Pipeline] sh
+ ./deploy.sh staging
Deploying checkout-service to staging environment...
Deployment complete. Version: 2.4.1-SNAPSHOT
[Pipeline] echo
Deployed branch feature/add-retry-logic to STAGING
[Pipeline] }
[Pipeline] stage (post - success)
[Pipeline] echo
Build and deploy complete for branch: feature/add-retry-logic
[Pipeline] End of Pipeline
Finished: SUCCESS
What just happened?
git checkout feature/add-retry-logic— Jenkins checked out the feature branch, not main. This meansenv.BRANCH_NAMEwill befeature/add-retry-logicwhen the script block evaluates it.[Pipeline] script— this line in the console output marks where Jenkins entered thescript { }block and started executing raw Groovy code.+ ./deploy.sh staging— theifcondition evaluatedenv.BRANCH_NAME == 'main'as false, so theelsebranch ran and called./deploy.sh staging. The correct deploy target was chosen automatically based on the branch name.Deployed branch feature/add-retry-logic to STAGING— the echo statement used Groovy string interpolation with${env.BRANCH_NAME}to print the actual branch name dynamically.- The post success block — ran because all stages passed, and again used
${env.BRANCH_NAME}to confirm which branch was built. This kind of dynamic logging is impossible in Freestyle jobs but trivial in Pipelines.
Picking the Right Syntax
Use Declarative when:
- Starting any new pipeline from scratch
- Your team includes non-Groovy developers
- You want Jenkins to validate your syntax before running
- You need the built-in
post,when,optionsdirectives - You want the stage view to work cleanly in the Jenkins UI
Use Scripted when:
- Maintaining an existing Scripted pipeline
- You need complex dynamic stage generation
- The logic genuinely cannot fit in a
script { }block - You're building shared library utilities (advanced — Section IV)
The trap most teams fall into
Teams start with Scripted because it feels more like "real code" and seems more powerful. Six months later, their pipelines are 400-line Groovy files that only one person understands. Declarative with script { } blocks handles 95% of real-world needs with far better readability. Use Scripted only when you've genuinely hit Declarative's limits.
Teacher's Note
In five years of reading Jenkins pipelines, I've seen maybe a handful of cases where Scripted was genuinely necessary. Write Declarative. Use script { } when you need power. Leave Scripted for the legacy code you inherit.
Practice Questions
1. What keyword does a Scripted Pipeline start with instead of pipeline?
2. What block do you open inside a Declarative pipeline's steps when you need to write raw Groovy logic?
3. Which Jenkins built-in environment variable holds the name of the Git branch that triggered the current build?
Quiz
1. What is one key advantage Declarative Pipeline has over Scripted Pipeline when it comes to errors?
2. In a Scripted Pipeline, how do you handle post-build actions like sending a failure notification?
3. When is it appropriate to use Scripted Pipeline syntax in a new project?
Up Next · Lesson 13
Jenkinsfile Introduction
The Jenkinsfile in depth — structure, best practices, how it connects to your repo, and the patterns used in real production pipelines.