Jenkins Declarative vs Scripted Pipeline: Syntax, Tradeoffs, and Examples
The Difference in One Sentence
Declarative Pipeline gives a Jenkinsfile a validated, opinionated structure; Scripted Pipeline exposes Jenkins Pipeline steps through a general Groovy control flow. Both ultimately run on the same Pipeline engine, but they optimize for different things.
For most application delivery pipelines, start with Declarative. Its stage model is readable in reviews, invalid placement is rejected early, and features such as post, when, options, matrix, and stage-level agents have explicit homes. Use Scripted when the workflow itself must be generated dynamically and cannot be expressed cleanly with Declarative directives.
You can practice both models in ShellGenius Jenkins Labs, beginning with Repair the First Pipeline and Repair a Scripted Pipeline.
---
The Same Delivery Flow in Declarative Syntax
pipeline {
agent { label 'linux-docker' }
options {
timestamps()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
parameters {
choice(name: 'TARGET', choices: ['staging', 'production'])
}
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Test') {
steps { sh 'npm ci && npm test' }
post { always { junit 'reports/*.xml' } }
}
stage('Deploy') {
when { branch 'main' }
steps {
sh './deploy.sh "$TARGET"'
}
}
}
post {
always { deleteDir() }
failure { echo 'Delivery failed; inspect the first failed stage' }
}
}
The structure is the feature. Jenkins can validate that stages contains stage blocks, that a stage contains one of steps, parallel, matrix, or stages, and that post conditions are in a supported location. A reviewer can find retention, timeouts, deployment gates, and cleanup without tracing arbitrary control flow.
---
The Equivalent Scripted Pipeline
def target = params.TARGET ?: 'staging'
node('linux-docker') {
timestamps {
timeout(time: 30, unit: 'MINUTES') {
try {
stage('Checkout') {
checkout scm
}
stage('Test') {
try {
sh 'npm ci && npm test'
} finally {
junit testResults: 'reports/*.xml', allowEmptyResults: true
}
}
if (env.BRANCH_NAME == 'main') {
stage('Deploy') {
sh "./deploy.sh ${target}"
}
}
} catch (err) {
currentBuild.result = 'FAILURE'
throw err
} finally {
deleteDir()
}
}
}
}
Scripted syntax makes ordinary programming constructs natural: loops, maps, methods, try/catch/finally, and dynamically created branches. It also makes it easier to build a Jenkinsfile that only its author understands. There is less structural validation, and code outside node may execute on the controller rather than an agent.
---
Where Each Construct Lives
| Need | Declarative | Scripted |
|---|---|---|
| Allocate an agent | agent { label 'linux' } | node('linux') |
| Run commands | Inside steps | Directly inside stage or node |
| Conditional stage | when | Groovy if |
| Cleanup | post { always { ... } } | try/finally |
| Controlled failure | catchError in steps | try/catch or catchError |
| Static fan-out | Declarative parallel or matrix | parallel map |
| Dynamic fan-out | script { parallel map } | Native parallel map |
Declarative is not “Pipeline without Groovy.†Expressions, shared-library calls, and a script step still use Groovy. The useful distinction is how much freedom the top-level Jenkinsfile permits.
---
A Good Hybrid Pattern
Keep orchestration Declarative and isolate the small part that genuinely needs programming:
stage('Component Tests') {
steps {
script {
def branches = [:]
['api', 'worker', 'web'].each { component ->
def name = component
branches[name] = {
sh "./ci/test-component.sh ${name}"
}
}
parallel branches
}
}
}
The list-to-map conversion belongs in script; checkout, timeouts, credentials, promotion, and cleanup remain visible as Declarative structure. Put heavy parsing or business logic in a versioned script executed on the agent, not in Groovy on the Jenkins controller.
---
Which Should You Learn First?
Learn Declarative first if you build normal CI/CD flows. Be fluent in agent, environment, options, parameters, when, parallel, matrix, and post. Then learn enough Scripted Pipeline to read node, construct a parallel map, and implement reliable try/catch/finally recovery.
Choose fully Scripted Pipeline only when the pipeline topology is truly data-driven, when maintaining an older Scripted estate, or when a required control flow has no clear Declarative representation. Complexity alone is not a reason: a complicated release process usually benefits from stronger structure, smaller shell tools, and a shared library.
Continue with the Jenkins theory course, then compare the solutions in Generate Parallel Work Programmatically and Guarantee Scripted Cleanup.