Jenkins Scripted Pipeline with Groovy
Scripted Pipeline gives you full Groovy programming power inside Jenkins. While Declarative Pipeline uses a fixed structure, Scripted Pipeline lets you write logic, loops, and conditions using code. This makes it suitable for complex, dynamic workflows.
Think of Declarative Pipeline as a pre-designed house with fixed rooms in fixed positions. Scripted Pipeline is an open plot of land where you design the entire floor plan yourself. More freedom, but more responsibility.
The node Block
Every Scripted Pipeline starts with a node block. The node block allocates a build executor on a Jenkins agent and runs all the code inside it.
// Run on any available agent
node {
stage('Build') {
sh 'mvn clean package'
}
}
// Run on an agent with a specific label
node('linux-builder') {
stage('Build') {
sh 'mvn clean package'
}
}
Stages in Scripted Pipeline
Stages in Scripted Pipeline are created with the stage function. They appear in the Jenkins stage view, just like in Declarative Pipeline. The difference is that stages are not enforced by structure — you can place them anywhere, nest them, or create them dynamically.
node {
stage('Checkout') {
git branch: 'main',
url: 'https://github.com/myteam/myapp.git'
}
stage('Build') {
sh 'mvn clean compile'
}
stage('Test') {
sh 'mvn test'
}
stage('Package') {
sh 'mvn package -DskipTests'
archiveArtifacts 'target/*.jar'
}
}
Variables and Groovy Expressions
In Scripted Pipeline, you use standard Groovy to define and work with variables.
node {
def appName = 'payment-service'
def buildDate = new Date().format('yyyyMMdd')
def version = "1.0.${env.BUILD_NUMBER}"
def deployHost = env.BRANCH_NAME == 'main' ? 'prod-server' : 'staging-server'
stage('Info') {
echo "App: ${appName} | Version: ${version} | Date: ${buildDate}"
echo "Will deploy to: ${deployHost}"
}
stage('Build') {
sh "mvn clean package -Dapp.version=${version}"
}
}
Conditional Logic
Use standard Groovy if/else statements to control the pipeline flow.
node {
stage('Build') {
sh 'mvn clean package'
}
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
echo 'Deploying to PRODUCTION'
sh 'deploy.sh production'
} else if (env.BRANCH_NAME == 'develop') {
echo 'Deploying to STAGING'
sh 'deploy.sh staging'
} else {
echo "Branch ${env.BRANCH_NAME} — no deployment configured"
}
}
}
Loops in Scripted Pipeline
Loops are not possible in Declarative Pipeline. In Scripted Pipeline, you use standard Groovy loops to perform repeated operations.
node {
// Deploy to multiple servers
def servers = ['server-1', 'server-2', 'server-3']
stage('Deploy to All Servers') {
for (server in servers) {
stage("Deploy to ${server}") {
echo "Deploying to ${server}..."
sh "scp target/myapp.jar deploy@${server}:/opt/app/"
sh "ssh deploy@${server} 'systemctl restart myapp'"
}
}
}
}
Diagram: Dynamic stage creation servers = ['server-1', 'server-2', 'server-3'] Pipeline Stages Generated: Checkout Build Test Deploy to server-1 ← Created by loop Deploy to server-2 ← Created by loop Deploy to server-3 ← Created by loop
Try-Catch for Error Handling
Scripted Pipeline handles errors using try-catch blocks — the standard Groovy error handling approach. This gives you precise control over what happens when a step fails.
node {
stage('Build') {
try {
sh 'mvn clean package'
echo 'Build succeeded'
} catch (Exception e) {
echo "Build failed with error: ${e.getMessage()}"
currentBuild.result = 'FAILURE'
throw e // Re-throw to mark the build as failed
} finally {
// This runs whether the build passed or failed
junit 'target/surefire-reports/*.xml'
cleanWs()
}
}
}
The finally block is equivalent to Declarative Pipeline's post { always {} } block. It runs regardless of whether the try block succeeded or threw an error.
Parallel Execution in Scripted Pipeline
Use the parallel step to run tasks simultaneously. Provide a map of stage names to closures.
node {
stage('Build') {
sh 'mvn clean package -DskipTests'
}
stage('Run Tests in Parallel') {
parallel(
'Unit Tests': {
node {
sh 'mvn test -Dtest.type=unit'
junit 'target/surefire-reports/*.xml'
}
},
'Integration Tests': {
node {
sh 'mvn test -Dtest.type=integration'
junit 'target/failsafe-reports/*.xml'
}
},
'Performance Tests': {
node {
sh 'gatling.sh -s LoadSimulation'
}
}
)
}
}
Using the script Block Inside Declarative Pipeline
You do not need a fully Scripted Pipeline to use Groovy logic. Inside Declarative Pipeline, wrap Groovy code in a script block. This lets you use loops, conditions, and Groovy expressions without switching to full Scripted syntax.
pipeline {
agent any
stages {
stage('Smart Deploy') {
steps {
script {
// Groovy code inside Declarative Pipeline
def targets = ['us-east', 'eu-west', 'ap-south']
for (region in targets) {
echo "Deploying to ${region}"
sh "deploy.sh --region ${region}"
}
}
}
}
}
}
This hybrid approach is the most practical solution. Use Declarative Pipeline for structure and readability, and use script blocks for logic where needed.
Common Built-in Pipeline Steps
| Step | What It Does |
|---|---|
| sh 'command' | Run a shell command (Linux/Mac) |
| bat 'command' | Run a Windows batch command |
| echo 'message' | Print text to the console |
| git url: '...', branch: '...' | Check out a Git repository |
| archiveArtifacts 'path' | Save files as build artifacts |
| junit 'path/*.xml' | Publish test results |
| cleanWs() | Delete the workspace |
| input 'message' | Pause and wait for human approval |
| sleep 30 | Pause for 30 seconds |
| currentBuild.result = 'FAILURE' | Set the build result programmatically |
Key Points
- Scripted Pipeline uses Groovy code inside a node block, giving full programming flexibility.
- Use variables, if/else conditions, and loops to create dynamic pipelines.
- Error handling with try-catch gives precise control over failure behavior.
- The script block inside Declarative Pipeline lets you add Groovy logic without switching to full Scripted syntax.
- Most teams use Declarative Pipeline with script blocks rather than fully Scripted Pipeline.
