Scripted Pipeline, Groovy Control Flow, node Blocks, Dynamic Logic, and When to Use Scripted Pipelines: Configuration, Design Choices, and Tradeoffs
Choose between Declarative and Scripted Pipeline, separate Pipeline steps from ordinary Groovy, size node scopes intentionally, and balance dynamic stage generation against auditability and restartability.
Learning objectives
- Choose Declarative or Scripted from the actual orchestration requirement rather than stylistic preference.
- Separate Pipeline steps from ordinary Groovy helpers and understand the CPS implications of each.
- Choose node boundaries based on resource/trust/workspace needs.
- Evaluate dynamic stages against visualization, auditability, security, and operational cost.
- Document version, plugin, trust, and recovery assumptions for Scripted automation.
1. The first design decision: do you need Scripted at all?
Scripted Pipeline is most valuable when the orchestration graph genuinely depends on validated runtime data. It is not automatically “more advanced.” A static workflow written in Scripted form can be harder to review because the reader must infer structure from general Groovy control flow rather than from Declarative grammar.
| Need | Better default | Why |
|---|---|---|
| Known stages and standard conditions | Declarative | Stronger structural validation and easier review. |
| Validated dynamic component/task list | Scripted or tightly bounded Scripted escape | General control flow can express the changing graph directly. |
| Large computation/data processing | Agent-side program | Pipeline Groovy is orchestration, not an application runtime. |
| Controller administration | JCasC/REST/CLI/maintained plugin/admin procedure | Do not normalize powerful internal API access inside build scripts. |
2. Pipeline step versus ordinary Groovy method
A Pipeline step participates in Jenkins execution semantics: it may schedule work, suspend, persist state, interact with credentials, or create build evidence. An ordinary Groovy method merely computes. That distinction matters more than the syntax.
def normalizedTargets(String raw) {
raw.split(',')
.collect { it.trim() }
.findAll { it }
.unique()
}
def targets = normalizedTargets('lint, unit, lint')
echo "targets=${targets.join(',')}"
This helper returns serializable strings/lists. It is small and
deterministic. It does not call node,
echo, sh, or other Pipeline steps.
3. When @NonCPS is appropriate—and when it is not
@NonCPS skips CPS transformation for a method. It can
help with certain pure Groovy/library operations that do not behave
correctly under CPS, but the method cannot call CPS-transformed
Pipeline steps. Inputs and outputs should be simple serializable
values.
@NonCPS
def compactLabels(List labels) {
labels.collect { it.toString().trim() }.findAll { it }.unique().sort()
}
def labels = compactLabels(['linux', 'linux', 'x86_64'])
echo "labels=${labels.join(',')}"
@NonCPS and then trying to call sh,
sleep, node, or echo from it
breaks the execution model. @NonCPS is not an “escape
from Jenkins.”
4. One large node block versus targeted allocations
| Design | Strength | Cost/risk | Evidence to inspect |
|---|---|---|---|
| One node around whole run | Simple workspace continuity | Holds executor during waits/controller-only logic | Executor occupancy, elapsed time, workspace state |
| Targeted nodes per phase | Releases capacity; supports different labels/trust zones | Workspace may differ; queue waits repeat | Queue IDs, node names, stash/artifact transfer |
| Separate privileged node | Isolates signing/deployment power | Requires explicit transfer and credential boundaries | Agent label, credential scope, artifact digest |
Node boundaries are infrastructure boundaries. Design them from executor capacity, workspace dependency, toolchain, network, and credential trust—not from indentation preference.
5. Dynamic stages versus static readability
A dynamic stage for each validated component can make a heterogeneous build easy to visualize. It can also produce unstable dashboards, unbounded stage counts, difficult trend comparison, and controller overhead when the list is large. Cap cardinality and normalize names.
def components = ['api', 'worker', 'web']
if (components.size() > 10) {
error "component count ${components.size()} exceeds lab limit"
}
components.each { component ->
stage("Test ${component}") {
node('lab-linux') {
sh "./test-component '${component}'"
}
}
}
In production, validate component before shell use and
consider whether parallelism belongs here. Chapter 21 treats
parallel and matrix capacity in depth.
6. Sandbox versus unsandboxed execution
Sandboxed Pipeline limits direct access to Jenkins/controller APIs. Approving an unsafe signature expands what Pipeline authors can do on the controller. That is a governance decision with security impact, not a routine development step. Trusted Shared Libraries can also execute privileged logic depending on configuration, so moving code into a library does not erase the trust question.
7. Restartability changes design choices
A Scripted Pipeline can survive supported controller restarts because Pipeline program state is persisted around asynchronous operations. Restartability does not mean every object is serializable or every external side effect is repeat-safe. Record side-effect identifiers before a suspension/restart and design idempotency explicitly.
For example, creating an external release and then sleeping is risky if a later rerun cannot tell whether the release already exists. Persist the release ID as build evidence and make the external operation create-or-update or otherwise reconcile safely.
8. Worked scenario: twelve components, two trust zones
A repository contains a validated manifest of 12 components. Ten require ordinary tests; two require signing on a restricted agent.
- Parse/validate the manifest into simple serializable component records.
- Reject more than the documented maximum cardinality.
- Use ordinary
lab-linuxagents for tests. - Archive immutable package digests.
- Move only approved digests to a restricted signing stage/agent.
- Bind signing credentials only inside the restricted node/stage.
- Record component, source SHA, unsigned/signed digest, node identity, and signing result.
Scripted Pipeline is defensible here if the manifest truly varies and the dynamic stages improve evidence. If the 12 stages are stable, Declarative may still be simpler.
9. Decision table
| Question | If yes | If no |
|---|---|---|
| Does the stage graph vary from validated runtime/source data? | Consider Scripted. | Prefer Declarative. |
| Does logic need workspace/tool/credential state? | Put it in the narrow node context. | Keep it outside node. |
| Must a value survive a suspension? | Keep it serializable and small. | Temporary object may remain local to pure computation. |
| Does code need internal Jenkins APIs? | Reconsider the mechanism/trust boundary. | Stay sandboxed. |
| Will dynamic stage count be large? | Bound it or redesign. | Dynamic stages may remain readable. |
10. Mini ADR exercise
Write a short architecture decision record with: requirement, why Declarative is insufficient or sufficient, source of dynamic data, maximum cardinality, node labels/trust zones, credential boundaries, serializable state model, sandbox assumptions, restart/side-effect strategy, plugin versions, rollback path, and observable evidence. If you cannot justify those points, the Scripted design is probably not mature enough.
Knowledge check
Why is a Pipeline step different from an ordinary Groovy helper?
A Pipeline step participates in Jenkins execution/durability/state semantics; an ordinary helper only computes in the Groovy program.
What is the safest use of @NonCPS?
A small pure helper that performs a necessary non-CPS transformation and returns simple serializable data without calling Pipeline steps.
What determines node scope?
Workspace/toolchain needs, executor capacity, trust/credential boundaries, network access, and file-transfer requirements—not code indentation.
Why can dynamic stages become an operational problem?
Unbounded or unstable stage graphs increase controller load, reduce trend readability, and make evidence harder to compare.
What does approving a sandbox signature change?
It expands what Pipeline code may invoke on the controller, so it changes the security/trust boundary.
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.