Jenkins Declarative Pipeline Syntax

Declarative Pipeline is the standard way to write Jenkins Pipelines. Its structured syntax makes pipelines readable, maintainable, and easy to validate. This topic covers every section of Declarative Pipeline in detail with practical examples.

The Top-Level Structure

Every Declarative Pipeline follows the same skeleton. Each block has a specific role and appears in a fixed order.

pipeline {
    agent { ... }          // WHERE to run

    environment { ... }    // VARIABLES available to all stages

    options { ... }        // SETTINGS for the pipeline

    parameters { ... }     // USER INPUTS before build starts

    triggers { ... }       // WHAT STARTS the pipeline

    stages {               // THE WORK to perform
        stage('name') {
            agent { ... }  // Optional: override agent for this stage
            steps { ... }  // Commands to run
            post  { ... }  // What to do after this stage
        }
    }

    post { ... }           // What to do after ALL stages finish
}

The agent Block

The agent block tells Jenkins where to run the pipeline or a specific stage. It is required at the top level.

// Run on any available Jenkins node
agent any

// Run on a specific labeled node
agent { label 'linux-builder' }

// Run inside a Docker container
agent {
    docker {
        image 'maven:3.9-eclipse-temurin-17'
        args '-v /root/.m2:/root/.m2'
    }
}

// No default agent (each stage must define its own)
agent none

The environment Block

The environment block defines variables available throughout the pipeline. Variables set here are accessible in every stage's steps.

environment {
    APP_NAME    = 'my-web-app'
    BUILD_ENV   = 'production'
    DEPLOY_DIR  = '/opt/apps/myapp'
    DOCKER_TAG  = "${env.BUILD_NUMBER}"  // Use Jenkins built-in variable
}

// Access in steps:
steps {
    sh "echo Deploying ${APP_NAME} to ${DEPLOY_DIR}"
    sh "docker build -t myimage:${DOCKER_TAG} ."
}

The options Block

Options configure pipeline-level settings that control behavior.

options {
    // Fail if pipeline runs longer than 30 minutes
    timeout(time: 30, unit: 'MINUTES')

    // Keep only last 10 build records
    buildDiscarder(logRotator(numToKeepStr: '10'))

    // Do not run two instances of this pipeline at the same time
    disableConcurrentBuilds()

    // Show timestamps in console output
    timestamps()

    // Retry the entire pipeline 2 times on failure
    retry(2)
}

The stages and stage Blocks

Stages organize your pipeline into logical phases. Each stage has a name and a steps block containing the commands to run.

stages {
    stage('Checkout') {
        steps {
            git branch: 'main',
                url: 'https://github.com/myteam/myapp.git',
                credentialsId: 'github-token'
        }
    }

    stage('Build') {
        steps {
            sh 'mvn clean compile -q'
            echo 'Compilation complete'
        }
    }

    stage('Unit Tests') {
        steps {
            sh 'mvn test'
        }
    }

    stage('Package') {
        steps {
            sh 'mvn package -DskipTests'
            archiveArtifacts artifacts: 'target/*.jar', fingerprint: true
        }
    }

    stage('Deploy to Staging') {
        steps {
            sh 'scp target/*.jar deploy@staging-server:/opt/apps/'
            sh 'ssh deploy@staging-server "systemctl restart myapp"'
        }
    }
}

Parallel Stages

Run multiple stages at the same time to speed up your pipeline. If unit tests and static analysis take 10 minutes each, running them in parallel cuts the wait time to 10 minutes total instead of 20.

stage('Parallel Testing') {
    parallel {
        stage('Unit Tests') {
            steps {
                sh 'mvn test'
            }
        }
        stage('Static Analysis') {
            steps {
                sh 'mvn checkstyle:check'
            }
        }
        stage('Security Scan') {
            steps {
                sh 'dependency-check.sh --project myapp'
            }
        }
    }
}
Timeline with parallel stages:

Without parallel:
  Build: 5min → Unit Tests: 10min → Analysis: 10min → Total: 25min

With parallel:
  Build: 5min → [Unit Tests: 10min] + [Analysis: 10min] → Total: 15min
                           (runs simultaneously)

Conditional Execution with when

The when block runs a stage only when a condition is true. Use this to deploy only on the main branch, or to skip stages for pull requests.

stage('Deploy to Production') {
    when {
        branch 'main'  // Only run when on the main branch
    }
    steps {
        sh 'deploy-production.sh'
    }
}

stage('Deploy to Staging') {
    when {
        anyOf {
            branch 'develop'
            branch 'release/*'
        }
    }
    steps {
        sh 'deploy-staging.sh'
    }
}

stage('Skip on Weekends') {
    when {
        expression { return new Date().getDay() in [1,2,3,4,5] }
    }
    steps {
        sh 'run-report.sh'
    }
}

The post Block

The post block runs steps after all stages complete. It contains conditions that define what to do based on the build result.

post {
    always {
        // Runs regardless of success or failure
        junit 'target/surefire-reports/*.xml'
        cleanWs()  // Clean the workspace
    }
    success {
        // Runs only when all stages passed
        slackSend channel: '#builds',
                  message: "Build PASSED: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
    }
    failure {
        // Runs only when any stage failed
        mail to: 'team@company.com',
             subject: "FAILED: ${env.JOB_NAME}",
             body: "See details: ${env.BUILD_URL}"
    }
    unstable {
        // Runs when build is unstable (e.g., some tests failed)
        slackSend channel: '#builds',
                  message: "Build UNSTABLE — tests failing"
    }
    changed {
        // Runs when build result is different from the previous build
        echo 'Build status has changed from previous run'
    }
}

A Complete Declarative Pipeline Example

pipeline {
    agent any

    environment {
        APP_NAME = 'order-service'
        DOCKER_IMAGE = "myregistry/${APP_NAME}:${env.BUILD_NUMBER}"
    }

    options {
        timeout(time: 45, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '20'))
        disableConcurrentBuilds()
    }

    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            parallel {
                stage('Unit Tests') {
                    steps { sh 'mvn test' }
                }
                stage('Code Quality') {
                    steps { sh 'mvn checkstyle:check' }
                }
            }
        }

        stage('Docker Build') {
            steps {
                sh "docker build -t ${DOCKER_IMAGE} ."
            }
        }

        stage('Deploy') {
            when { branch 'main' }
            steps {
                sh "docker push ${DOCKER_IMAGE}"
                sh "kubectl set image deployment/${APP_NAME} app=${DOCKER_IMAGE}"
            }
        }
    }

    post {
        always {
            junit 'target/surefire-reports/*.xml'
            cleanWs()
        }
        failure {
            mail to: 'devteam@company.com',
                 subject: "Pipeline failed: ${env.JOB_NAME}",
                 body: "Check: ${env.BUILD_URL}"
        }
    }
}

Key Points

  • Declarative Pipeline uses a strict structure: pipeline → agent → stages → stage → steps.
  • The environment block defines variables available to all stages.
  • Use the options block to set timeouts, disable concurrent builds, and configure logs.
  • Parallel stages run simultaneously to speed up the pipeline.
  • The when block makes stages conditional — run only on specific branches or conditions.
  • The post block handles what happens after all stages: notifications, cleanup, and reporting.

Leave a Comment