Scripted Pipeline, Groovy Control Flow, node Blocks, Dynamic Logic, and When to Use Scripted Pipelines: Concepts, Architecture, and Mental Model
Understand Scripted Pipeline as a CPS-transformed Groovy orchestration program, distinguish controller-side control flow from agent-side work, and identify when dynamic logic justifies the additional flexibility.
Learning objectives
- Explain how Scripted Pipeline differs from Declarative while using the same Pipeline execution engine.
- Distinguish Groovy control-flow evaluation from Pipeline steps and agent-side shell commands.
- Explain what a node block schedules, which workspace it creates, and why node scope is a resource decision.
- Describe CPS persistence, suspension points, serialization constraints, and the safe purpose of @NonCPS.
- Recognize when dynamic orchestration benefits from Scripted Pipeline and when Declarative remains clearer.
1. Why Scripted Pipeline exists
Chapter 10 used Declarative Pipeline to make common delivery structure explicit and validated. That structure is valuable, but some orchestration is genuinely dynamic: a validated list of components may be discovered at run time, different trusted inputs may require different node allocations, or a bounded loop may need to create a variable number of stages. Scripted Pipeline exposes more of Groovy's control-flow vocabulary for those cases.
The extra flexibility has a cost. A Scripted Jenkinsfile is not an ordinary Groovy program and it is not a shell script. Jenkins transforms most Pipeline Groovy into continuation-passing style (CPS) so it can save execution state around asynchronous Pipeline steps. That means you must reason about where code executes, what state must be serializable, when a node/workspace exists, and which APIs the Groovy sandbox permits.
2. Mental model: CPS program → decisions → node work → durable state
A Scripted Jenkinsfile is compiled into Pipeline program state on
the controller. Groovy expressions make orchestration decisions.
Pipeline steps can suspend and resume. A node step
enters the queue, waits for an eligible executor, and creates a
workspace on the selected agent. Inside that context,
sh/bat start external processes on the
agent. Jenkins persists enough Pipeline state to resume supported
work after expected interruptions.
flowchart TD
A[Jenkinsfile at exact SCM SHA] --> B[Groovy CPS program on controller]
B --> C{Groovy control-flow decision}
C --> D[Pipeline step]
D --> E{node needed?}
E -->|yes| F[Queue + label eligibility]
F --> G[Executor + agent workspace]
G --> H[sh/bat/tool process on agent]
E -->|no| I[Controller-orchestrated Pipeline step]
H --> J[Step result / files]
I --> J
J --> K[Persisted Pipeline run state]
K --> L[Stage/build result + artifacts/evidence]
Do not interpret the diagram as “all Groovy is expensive controller work.” Most orchestration code is brief. The risk appears when large computations, huge in-memory objects, or controller APIs are used as if Jenkins were a general application server.
3. The smallest useful Scripted Pipeline
node('lab-linux') {
stage('Observe') {
echo "job=${env.JOB_NAME} build=${env.BUILD_NUMBER}"
sh 'printf "node=%s\\nworkspace=%s\\n" "$NODE_NAME" "$WORKSPACE"'
}
}
node('lab-linux') is a Pipeline step. It queues this
body for an agent matching the label and gives the body a workspace.
stage provides a named visualization/evidence boundary.
echo is a Pipeline step. sh asks the agent
to start a shell process. The shell receives environment variables
such as NODE_NAME and WORKSPACE; Groovy
does not become the shell.
4. Ordinary Groovy versus Pipeline steps
| Construct | Layer | Can suspend? | Operational meaning |
|---|---|---|---|
if, for, list/map operations
|
CPS-transformed Groovy program | Not by themselves | Decide orchestration flow; keep bounded. |
node, sleep, input,
checkout
|
Pipeline steps | Often yes | Interact with Jenkins state and may persist/resume. |
sh/bat |
Pipeline step + agent process | Durable step | Runs project/tool logic on the selected agent. |
| Java/Jenkins internal API call | Controller JVM | Not a normal Pipeline step | Potentially powerful/unsafe; sandbox may reject it. |
@NonCPS helper |
Normal Groovy execution for that method | No Pipeline suspension inside | Use only for small pure transformations that do not call Pipeline steps. |
5. Bounded dynamic control flow
Scripted Pipeline can use familiar if/else, loops,
maps, closures, and functions. The important word is
bounded. The set of work should come from trusted or
validated inputs, stage names should remain intelligible, and the
resulting graph should not explode into hundreds of
controller-managed branches accidentally.
def requested = ['lint', 'unit']
def allowed = ['lint', 'unit', 'package'] as Set
def selected = requested.findAll { allowed.contains(it) }
node('lab-linux') {
selected.each { taskName ->
stage("Check: ${taskName}") {
sh "./lab-task '${taskName}'"
}
}
}
For an actual untrusted parameter, do not interpolate it into a shell command like this. Validate against an allowlist and pass it through a safer boundary. The example uses fixed synthetic values to focus on stage generation.
6. A node block is a scheduling and workspace decision
One large node block is easy to read but can hold an
executor while controller-only decisions or waits occur. Several
targeted node blocks can free capacity between phases
and select different labels, but each allocation may use a different
workspace or agent. If later phases require files, transfer them
intentionally with artifacts/stashes or regenerate only when that
behavior is explicitly safe.
7. CPS, suspension, and serializable state
Most Pipeline Groovy is CPS-transformed. When a Pipeline reaches an
asynchronous step, Jenkins can persist the program state. Variables
that remain reachable across such a suspension may need to be
serializable. Keeping a non-serializable object such as a regex
Matcher, an open stream, or many third-party objects
across a sleep, input, node,
or other suspension point can produce
NotSerializableException or confusing CPS behavior.
// Good pattern: reduce temporary object to a serializable String before suspension.
def releaseText = 'release-42'
def releaseId = (releaseText =~ /release-(\d+)/)[0][1].toString()
sleep 1
echo "releaseId=${releaseId}"
@NonCPS is not a blanket performance or compatibility
fix. A method marked @NonCPS must not call Pipeline
steps such as echo, sh, node,
or sleep. Prefer simple serializable data structures
and external tools on agents for substantial computation.
8. Sandbox and script approval are trust controls
Pipeline scripts normally run in the Groovy Sandbox. When code attempts a method or API that is not approved, Script Security can reject the signature. That rejection is evidence of a trust boundary, not an instruction to approve everything. An unsandboxed/approved script may call powerful Jenkins controller APIs and therefore belongs to a much stronger administrative trust domain.
9. When Scripted is justified
| Situation | Prefer | Reason |
|---|---|---|
| Static build/test/package/deploy stages | Declarative | Validation and predictable structure are valuable. |
| Small conditional stage | Declarative when |
No need for a general-purpose control-flow model. |
| Dynamic stage list from a validated local manifest |
Scripted or a tightly bounded Declarative
script escape
|
Dynamic orchestration is the actual requirement. |
| Large data transformation | Agent-side program | Do not turn controller-side Pipeline Groovy into an application workload. |
| Direct Jenkins internal API administration | Dedicated admin mechanism | Pipeline is not a safe substitute for JCasC, supported APIs, or maintained plugins. |
10. Read-only inspection before editing
- Record Jenkins core/Java and Pipeline: Groovy, Script Security, and Nodes/Processes plugin versions.
- Record job full name, build number/URL, cause, and exact Jenkinsfile/source SHA.
-
Inspect existing
nodelabels and agent/workspace evidence. - Identify every Groovy variable that may remain live across Pipeline steps.
- List any sandbox rejection or approved signature without approving anything new.
- Record stage names and build result so later dynamic changes can be compared.
Knowledge check
What does node('lab-linux') do?
It adds the enclosed work to the Jenkins queue for an eligible node, acquires an executor, and creates/uses a workspace for that Pipeline context.
Why is Scripted Pipeline not ordinary Groovy?
Most Pipeline Groovy is CPS-transformed so execution state can be suspended/persisted/resumed around Pipeline steps, which introduces serialization and method-mismatch constraints.
Where should substantial build computation run?
In external tools/processes on agents, typically through
sh/bat, rather than as large
controller-side Groovy computations.
What should you do when the Groovy sandbox rejects an internal API call?
Treat it as a trust signal; prefer a supported Pipeline step/plugin/API or reviewed abstraction rather than blindly approving the signature.
When is Scripted Pipeline most defensible?
When bounded dynamic orchestration is a real requirement and cannot be expressed clearly with normal Declarative constructs without embedding equivalent Scripted logic.
Official references and version notes
- Jenkins LTS changelog — current LTS and tested Java configurations.
-
Jenkins Pipeline handbook
— Scripted Pipeline fundamentals,
node, stages, and Jenkinsfile concepts. - Pipeline Syntax — Scripted control-flow examples and Pipeline syntax reference.
- Pipeline Best Practices — controller-side Groovy, serialization, and Pipeline scalability guidance.
-
Pipeline CPS Method Mismatches
— CPS transformation,
@NonCPS, serialization, and mismatch failure modes. - In-process Script Approval — Groovy Sandbox and script/signature approval security model.
- Pipeline: Groovy — current CPS execution engine implementation and compatibility.
- Script Security — current sandbox/approval plugin baseline and security history.
-
Pipeline: Nodes and Processes
—
node, workspace, and durable external-process support.
Rechecked on 2026-09-16. Examples assume
Jenkins 2.568.3 LTS, tested with Java 21 and 25,
Pipeline: Groovy 4380.v6eb_8378b_9647, Script
Security 1415.v9a_f9b_3a_c253d, and Pipeline:
Nodes and Processes 1479.v56e587f413a_7.
Pipeline: Groovy 4380 requires Jenkins 2.528.3 or newer. The
mandatory lab uses a disposable lab-linux agent, a
synthetic repository, no production credential, and no unsandboxed
approval. Record your actually installed versions because plugin
releases change independently from Jenkins core.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.