Jenkins Lesson 12 – Declarative vs Scripted | Dataplexa
Section II · Lesson 12

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 steps that 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 means env.BRANCH_NAME will be feature/add-retry-logic when the script block evaluates it.
  • [Pipeline] script — this line in the console output marks where Jenkins entered the script { } block and started executing raw Groovy code.
  • + ./deploy.sh staging — the if condition evaluated env.BRANCH_NAME == 'main' as false, so the else branch 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, options directives
  • 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.