Chapter 13Lesson 01~125 minutes

Groovy CPS, Serialization, @NonCPS, Pipeline State, Restarts, and Common Pipeline Programming Pitfalls: Concepts, Architecture, and Mental Model

Understand why Jenkins transforms Pipeline Groovy into continuation-passing style, what must be serializable at suspension points, how persisted run state resumes after restart, and where @NonCPS changes the execution contract.

Groovy CPSSerialization@NonCPSprogram.datRestartController state

Learning objectives

  • Explain CPS transformation, continuations, suspension points, serialized Pipeline state, and rehydration after restart.
  • Distinguish CPS-transformed Pipeline code from native Java/Groovy methods and @NonCPS helpers.
  • Identify which variables and objects can become part of persisted Pipeline state and why non-serializable objects can fail.
  • Explain why Pipeline Groovy is controller-side orchestration even when a node block allocates an agent.
  • Separate durable Jenkins run state from agent workspace files and external side effects.

1. Why ordinary Groovy intuition is not enough

Chapter 12 used steps such as sleep, input, retry, and waitUntil. Those steps can pause a run for seconds or hours, and the Jenkins controller may restart while the Pipeline is paused. A normal in-memory script would lose its call stack and local variables. Jenkins solves that problem by transforming most Pipeline Groovy into a resumable program.

This durability is powerful, but it creates constraints. A local variable that looks harmless in ordinary Groovy may become part of the continuation that Jenkins must serialize. A closure passed into a native Groovy method may cross from CPS-transformed code into non-CPS code. Heavy loops may consume controller CPU even when they appear inside node. Understanding these boundaries turns mysterious Pipeline failures into diagnosable state transitions.

Core principle: Pipeline Groovy is orchestration code. Keep durable state small and simple, run build computation on agents or external tools, and treat controller persistence as an implementation boundary you observe—not a file format you edit.

2. Mental model: source → CPS → persisted continuation → resume

Jenkins first compiles the Pipeline script, but most Pipeline code is transformed into continuation-passing style (CPS). Instead of relying on a normal JVM stack frame that disappears on restart, Jenkins represents the next continuation explicitly. When an asynchronous Pipeline step suspends, Jenkins can persist the relevant program graph. After a restart, the run is loaded, persisted state is rehydrated, step execution is restored, and the continuation can proceed.

Pipeline CPS and restart path
flowchart TD
 A[Jenkinsfile at exact SCM revision] --> B[Groovy parse / compile]
 B --> C[CPS transformation]
 C --> D[Continuation and Pipeline state]
 D --> E{Asynchronous step / suspension}
 E -->|persist| F[Build run state + program.dat]
 F --> G[Controller restart]
 G --> H[WorkflowRun reload / rehydration]
 H --> I[Step onResume / continuation resumes]
 I --> J[Next Pipeline step]
 C --> K[@NonCPS method]
 K --> L[Native Groovy execution on controller]
 L --> M[Serializable summary returned to CPS code]

The side path matters: @NonCPS methods are compiled with ordinary Groovy semantics rather than CPS semantics. They may use non-serializable objects locally, but they must finish synchronously, must not call Pipeline steps, and should return only compact serializable values to the CPS program.

3. What is CPS-transformed?

Code category CPS? Operational consequence
Most Jenkinsfile / Shared Library Groovy Yes Can suspend; state may need serialization
Pipeline steps that take blocks Yes Participate in durable flow graph
Java/JDK/Groovy runtime bytecode No Runs natively; cannot call back into CPS arbitrarily
Pipeline-script constructors No Must not invoke CPS-transformed methods/steps
@NonCPS methods No No Pipeline steps; no suspension; return safe values

A legal direction is CPS code calling a normal helper and receiving a plain result. The dangerous direction is native/non-CPS code calling a CPS-transformed closure or Pipeline step.

4. Suspension points and serialization

Many asynchronous steps can suspend: sleep, input, agent allocation, shell steps, and many plugin-provided steps. At a persistence point, variables reachable from the continuation may need to be serialized. Primitive values, strings, and simple lists/maps of serializable values are usually safe. Runtime handles such as regex matchers, open streams, iterators, parser internals, or plugin objects may not be.

def state = [operation: 'cps-demo-001', attempt: 1]
echo "before=${state}"
sleep 3
echo "after=${state}"

This small map is intentionally boring. Boring state is good durable state: compact, inspectable, and easy to reason about across a restart.

5. program.dat is internal run state

The Pipeline: Groovy engine persists the program graph under the build record, commonly including a program.dat file. Its exact internal representation is not an operator API. You may verify that the build/run has durable state and record file metadata in a disposable controller, but do not edit or transplant program.dat as a repair technique.

# Read-only inspection on the disposable controller only
find "$JENKINS_HOME/jobs" -path '*/builds/*/program.dat' -type f -printf '%p %s bytes\n' 2>/dev/null | tail
Recovery boundary: restore supported Jenkins state from a known-good backup and compatible plugin/core baseline. Do not repair a Pipeline by modifying serialized internals.

6. @NonCPS: a narrow synchronous boundary

A good @NonCPS helper performs bounded pure computation and returns a serializable summary:

@NonCPS
String extractBuildId(String text) {
  def matcher = text =~ /build-(\d+)/
  return matcher.find() ? matcher.group(1) : 'none'
}

def id = extractBuildId('release build-42 ready')
echo "id=${id}"

The regex matcher never escapes the native helper. The returned String is safe to retain. By contrast, putting echo, sh, sleep, node, or another Pipeline step inside the annotated method violates the execution contract.

7. Groovy logic still consumes controller resources

Pipeline orchestration code executes in the controller process. A node block allocates an executor/workspace for steps that need an agent, but an expensive Groovy sort, JSON transformation, or large in-memory aggregation is still controller-side unless you deliberately offload it.

node('lab-linux') {
  // Agent-side process: appropriate for substantial computation.
  sh './scripts/summarize-results.sh results.json > summary.txt'
  def summary = readFile('summary.txt').trim()
  echo "summary=${summary}"
}

Keep the Pipeline as glue: call tools on agents, retain only the compact result needed for orchestration, and archive larger evidence as files rather than carrying it through the CPS graph.

8. Restart durability does not make external side effects atomic

A resumed Pipeline remembers its Jenkins program state, but Jenkins cannot magically roll back or transactionally restore a package registry, deployment target, ticketing system, or database. If an external API performed an action and Jenkins lost the response before persisting the next continuation, the resumed run may not know whether the action happened.

Use immutable operation IDs, target-side idempotency keys, target-state verification, and compensation logic. Chapter 12’s side-effect guard remains essential even after you understand CPS internals.

9. Read-only evidence before modification

  • Record Jenkins core, Java, Pipeline: Groovy, Pipeline: Supporting APIs, and Script Security versions.
  • Record job full name, build number/URL, source/Jenkinsfile SHA, current stage/step, node/workspace, and build result.
  • Preserve the full exception class and the first useful stack frames before changing code.
  • Record whether a suspension point occurred immediately before the failure.
  • Record controller heap/CPU or thread evidence if the symptom is performance-related.
  • Verify external target state independently before retrying a side effect.

10. Durability settings are a trade-off, not a bug fix

Jenkins can trade disk I/O for Pipeline survivability using durability settings. Lower-durability modes persist program state less frequently. That can change when a serialization error becomes visible, but it does not make a non-serializable program correct. Do not switch to a performance-optimized durability mode merely to suppress NotSerializableException.

Next lesson

Guided Hands-On Workflow and Core Operations

Run small CPS-safe and CPS-broken examples, repair a serialization boundary with @NonCPS, and prove restart continuity on a disposable controller.

Knowledge check

Why can a local variable trigger a serialization failure?

What is the safe role of @NonCPS?

Does code inside node automatically execute all Groovy logic on the agent?

What should an operator do with program.dat?

What does restart durability not guarantee?

Official references and version notes

Version and compatibility note

Rechecked on 2026-09-16. Examples assume Jenkins 2.568.3 LTS (tested with Java 21 and 25), Pipeline: Groovy 4380.v6eb_8378b_9647, Pipeline: Supporting APIs 1015.v785e5a_b_b_8b_22, and Script Security 1422.v06869826dd9b_. The mandatory path is local/disposable, uses synthetic state and fake identities, and requires no commercial service. Plugin releases are independent of Jenkins core, so record the versions actually installed on your controller before applying these lessons.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.