Back to Jenkins
Q2Question 2 of 2

Jenkins Scripted vs Declarative Pipeline and Groovy's Role in a Jenkinsfile

Key Concepts
  • A Declarative pipeline uses a structured pipeline {} block with fixed sections like agent, stages, steps, and post.
  • A Scripted pipeline is written inside a node {} block as free-form Groovy with full imperative control.
  • Both styles run on the Jenkins Groovy engine, specifically a sandboxed CPS variant that survives controller restarts.
  • Declarative is easier to read, review, and validate; Scripted offers maximum flexibility at the cost of fewer guardrails.
  • Shared Libraries let you write reusable Groovy in a versioned repository and call it from either style.
  • The strong default for banking is Declarative, dropping into a script {} block or Shared Library for the rare bits of real logic.

Definition

Declarative is a structured Groovy DSL with guardrails for readability, Scripted is free-form Groovy for maximum flexibility, and Groovy is the engine running both.
  • Declarative pipeline – uses a top-level pipeline {} block with predefined sections (agent, stages, steps, environment, post, when, input) that Jenkins validates before execution.
  • Scripted pipeline – written inside a node {} block as a full Groovy program with variables, loops, conditionals, and functions; more flexible but more error-prone.
  • Groovy – the underlying language for both styles; Declarative is a Groovy-based DSL with guardrails, Scripted is closer to raw Groovy.
  • CPS sandbox – the Jenkins Groovy interpreter uses continuation-passing style so pipelines survive controller restarts, with a security sandbox blocking certain methods.
  • Shared Libraries – reusable Groovy code in a versioned repository, called from either style to keep Jenkinsfiles small.

Think of two ways to write the National Bank of India's cheque-processing procedure. The Declarative way is a standard printed form with fixed boxes for Branch, Steps, Approval, and On Failure. Anyone can fill it in correctly and a supervisor can check it at a glance. The Scripted way is a blank notebook where you write the procedure freehand with loops and conditionals. It can express anything but is easier to make mistakes in. Both are read by the same clerk (the Groovy engine) who carries out the instructions.

Understanding the Concepts

What is a Declarative pipeline?

A Declarative pipeline always starts with pipeline {} at the top level. Inside it you declare an agent, an environment block, a stages block with named stages, and each stage has a steps block. Jenkins validates the structure before execution, so a malformed pipeline is rejected early.

GROOVY
// Declarative: banking transfer service pipeline
pipeline {
    agent any
    environment {
        REGISTRY = 'registry.bank.local'
        SERVICE  = 'transfer-service'
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn verify'
            }
        }
        stage('Deploy') {
            when { branch 'main' }
            steps {
                sh "kubectl set image deployment/${SERVICE} ${SERVICE}=${REGISTRY}/${SERVICE}:${env.GIT_COMMIT}"
            }
        }
    }
    post {
        always  { junit '**/target/surefire-reports/*.xml' }
        failure { slackSend channel: '#bank-alerts', message: 'Build failed!' }
    }
}

What is a Scripted pipeline?

A Scripted pipeline starts with node {} and is a full Groovy program. You write imperative code directly: variables, conditionals, loops, and functions.

GROOVY
// Scripted: banking transfer service pipeline
node {
    def services = ['transfer-service', 'fraud-service', 'loan-service']
    def registry = 'registry.bank.local'

    stage('Checkout') {
        checkout scm
    }

    for (svc in services) {
        stage("Build ${svc}") {
            dir(svc) {
                sh 'mvn clean package'
            }
        }
    }

    if (env.BRANCH_NAME == 'main') {
        stage('Deploy') {
            sh "kubectl apply -f deploy/"
        }
    }
}

The loop over services and the conditional deploy are natural in Scripted but awkward in Declarative.

How do Declarative and Scripted compare?

| Feature | Declarative | Scripted |
|---|---|---|
| Top-level block | pipeline {} | node {} |
| Structure | Fixed sections (agent, stages, post) | Free-form Groovy |
| Validation | Pre-execution syntax check | Runtime errors only |
| Conditions | when { branch 'main' } | if (env.BRANCH_NAME == 'main') |
| Post-actions | Built-in post { always, success, failure } | Manual try/finally |
| Loops and logic | Limited, needs script {} block | Native Groovy |
| Readability | High, structured | Varies, depends on author |
| Recommended for | Most pipelines, audited environments | Dynamic, complex logic |

What is Groovy's role?

Groovy is the engine running both styles. Both are executed by the Jenkins Groovy interpreter. In a Declarative pipeline, you drop into Groovy with a script {} block when you need real logic.

GROOVY
// Declarative with a script block for Groovy logic
pipeline {
    agent any
    stages {
        stage('Tag') {
            steps {
                script {
                    def tag = sh(returnStdout: true,
                                 script: 'git rev-parse --short HEAD').trim()
                    env.IMAGE_TAG = tag
                    echo "Image tag: ${env.IMAGE_TAG}"
                }
            }
        }
    }
}

In a Scripted pipeline, the whole file is Groovy. Understanding Groovy matters because you use its string interpolation, collections, closures, and the sh, echo, and error steps that Jenkins exposes.

What are Shared Libraries?

Shared Libraries let you write reusable Groovy in a versioned repository and call it from either pipeline style. The vars/ directory holds global steps, and src/ holds regular Groovy classes.

GROOVY
// vars/bankPipeline.groovy in the Shared Library repo
def call(Map config) {
    pipeline {
        agent any
        stages {
            stage('Build') {
                steps { sh "mvn clean package -pl ${config.service}" }
            }
            stage('Deploy') {
                when { branch 'main' }
                steps { sh "kubectl apply -f deploy/${config.service}/" }
            }
        }
    }
}

// Each service's Jenkinsfile (2 lines):
@Library('bank-shared-lib@v3') _
bankPipeline(service: 'transfer-service')

What are common mistakes?

GROOVY
// WRONG: heavy Groovy computation on the controller.
// This runs on the Jenkins controller, not on the agent.
pipeline {
    agent any
    stages {
        stage('Compute') {
            steps {
                script {
                    // Parsing a 100MB file on the controller slows everything.
                    def data = readFile('huge-report.csv')
                    def lines = data.split('\n')
                    // ... expensive processing ...
                }
            }
        }
    }
}

// CORRECT: push heavy work to the agent with a shell step.
pipeline {
    agent any
    stages {
        stage('Compute') {
            steps {
                sh 'python3 process_report.py huge-report.csv'
            }
        }
    }
}
GROOVY
// WRONG: using Scripted for a simple, linear pipeline.
node {
    stage('Build') { sh 'mvn clean package' }
    stage('Test')  { sh 'mvn verify' }
    stage('Deploy') { sh 'kubectl apply -f deploy/' }
}
// No post-actions, no when-conditions, no validation.

// CORRECT: use Declarative for a simple, linear pipeline.
pipeline {
    agent any
    stages {
        stage('Build')  { steps { sh 'mvn clean package' } }
        stage('Test')   { steps { sh 'mvn verify' } }
        stage('Deploy') { when { branch 'main' } steps { sh 'kubectl apply -f deploy/' } }
    }
    post { failure { slackSend message: 'Build failed!' } }
}

When should you use each style?

The strong default for a banking environment is Declarative. Its structure is easy to read, review, and validate, and the standard sections cover almost everything. You drop into a script {} block for rare bits of logic and push complex reusable code into a Shared Library.

Scripted is reserved for genuinely dynamic needs, such as generating stages at runtime for an unknown number of services.

GROOVY
// Scripted: generating stages dynamically (Declarative cannot do this)
node {
    def services = sh(returnStdout: true, script: 'ls -d */').trim().split('\n')
    for (svc in services) {
        stage("Build ${svc}") {
            dir(svc) { sh 'mvn clean package' }
        }
    }
}

What should a senior engineer know?

  • The CPS sandbox blocks certain Groovy methods for security. Not all standard Groovy works in a pipeline.
  • Heavy in-pipeline Groovy computation runs on the controller, not the agent, and can slow the entire Jenkins instance.
  • Library code runs outside the sandbox by default, so it is the right home for anything non-trivial but must be carefully reviewed.
  • Pin the Shared Library to a version tag (@Library('bank-shared-lib@v3')) for reproducibility and audit.
  • Declarative pipelines support Blue Ocean visualization out of the box, which gives a clear stage view for non-developers.
GROOVY
// Senior pattern: pinned library, Declarative, minimal Jenkinsfile
@Library('bank-shared-lib@v3') _
bankPipeline(
    service: 'transfer-service',
    javaVersion: 21,
    deployTargets: ['dev', 'qa', 'prod']
)

Visual Overview

DIAGRAM
  Declarative Pipeline:
  ┌──────────────────────────────────────────┐
  │  pipeline {                               │
  │    agent any                              │
  │    environment { ... }                    │
  │    stages {                               │
  │      stage('Build') { steps { ... } }     │
  │      stage('Test')  { steps { ... } }     │
  │      stage('Deploy'){ when{} steps{...} } │
  │    }                                      │
  │    post { always{} failure{} }            │
  │  }                                        │
  └──────────────────────────────────────────┘
         Validated before execution

  Scripted Pipeline:
  ┌──────────────────────────────────────────┐
  │  node {                                   │
  │    def x = ...                            │
  │    if (...) { stage('A') { sh '...' } }   │
  │    for (s in list) { stage(s) { ... } }   │
  │  }                                        │
  └──────────────────────────────────────────┘
         Validated at runtime only

  Both run on:
  ┌──────────────────────────────────────────┐
  │  Jenkins Groovy Engine (CPS Sandbox)      │
  │  - Continuation-passing style             │
  │  - Survives controller restarts           │
  │  - Security sandbox blocks unsafe methods │
  └──────────────────────────────────────────┘

Key Takeaway

A Jenkinsfile defines a pipeline as code in two styles. Declarative uses a structured pipeline block with fixed sections like agent, stages, steps, and post. Jenkins validates its structure before execution, so a malformed pipeline is rejected early. Scripted is written as a full Groovy program inside a node block with variables, loops, and conditionals. It is far more flexible but has fewer guardrails, so mistakes surface only at runtime.

Groovy is the common foundation. Both styles run on the Jenkins Groovy engine, a sandboxed continuation-passing variant that lets pipelines survive controller restarts. In Declarative you drop into a script block for real logic, while Scripted is Groovy from top to bottom. Shared Libraries let you write reusable Groovy in a versioned repository and call it from either style, keeping Jenkinsfiles small and auditable.

My default in a banking environment is Declarative, because the structured form makes stages, conditions, and failure handling obvious at a glance, which matters for audit and review. I reserve Scripted for genuinely dynamic needs and push heavy logic into Shared Libraries so the pipeline file stays clean.

Remember one sentence: Declarative is the structured form with guardrails, Scripted is free-form Groovy, and both run on the same Groovy engine.

---

Share
Use ← → arrow keys to navigate