Home DevOps Jenkins Groovy: CPS Hell, @NonCPS, and Shared Libraries – A Survival Guide
Advanced ✅ Tested on Jenkins 2.440+ | Groovy Plugin 1.0+ | Jenkins Script Console 9 min · 2026-07-09

Jenkins Groovy: CPS Hell, @NonCPS, and Shared Libraries – A Survival Guide

Master Jenkins Groovy scripting: CPS transformation, @NonCPS, try/catch patterns, Script Console debugging, and shared libraries.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
July 09, 2026
last updated
230
articles · all by Naren
Before you start⏱ 30 min
  • Deep devops experience in a production environment
  • Familiarity with debugging, profiling, and performance analysis
  • Understanding of distributed systems and failure modes
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Groovy in Jenkins pipelines undergoes CPS transformation: methods are split into continuations for resumability, which breaks standard Java/Groovy constructs like loops and try/catch.
  • Use @NonCPS to mark methods that should run without CPS transformation – ideal for pure data processing, JSON parsing, or any code that doesn't need pipeline state.
  • Never use JsonSlurper or XmlSlurper inside a CPS-transformed method – they cause serialization errors. Instead, call sh '...' with jq or xmllint.
  • Shared libraries must be loaded with library 'my-lib@branch' and the Groovy code in vars/ is automatically CPS-transformed – use @NonCPS inside helper classes.
  • Script Console (/script) is your best friend for debugging: run println GroovySystem.version to check Groovy version, test snippets, and inspect serialization issues.
  • Always wrap pipeline steps in try/catch/finally to handle failures gracefully – but remember that CPS-transformed try/catch can't catch errors from @NonCPS methods if they throw.
  • Avoid storing complex objects (like ArrayList of HashMap) in pipeline variables – they must be serializable. Use @NonCPS methods to process data and return simple types.
  • For lists and maps, use def list = [] and def map = [:] – but be aware that closures inside CPS can cause NotSerializableException if they capture non-serializable state.
✦ Definition~90s read
What is Jenkins Groovy Scripting for Pipelines?

Jenkins Groovy scripting is the programming language behind both Scripted and Declarative Pipelines. When you write pipeline { ... } in a Jenkinsfile, it's actually Groovy code that Jenkins compiles into a workflow. Scripted Pipeline gives you full Groovy power – variables, loops, functions – while Declarative Pipeline is a more structured DSL built on top of the same engine.

Imagine you're baking a cake but the recipe is frozen mid-step every time the oven timer goes off.

The key concept is 'CPS transformation.' Jenkins uses the Groovy CPS library (org.jenkins-ci.plugins:workflow-cps) to rewrite your code. Every method call that could block (like sh, input, sleep) becomes a 'continuation' – the pipeline state is saved, and execution resumes later. This allows pipelines to be paused for hours and then continue exactly where they left off.

However, this transformation is not perfect. It only works on code that runs in the 'CPS thread' – typically pipeline steps and methods called from the pipeline DSL. Any code that uses standard Java/Groovy libraries (like java.io, groovy.json, java.util.stream) may fail because those classes aren't designed to be serialized mid-execution.

This is where @NonCPS comes in: it tells Jenkins to run that method without CPS transformation, bypassing the serialization requirement. But then you lose the ability to resume inside that method – it must complete atomically.

Plain-English First

Imagine you're baking a cake but the recipe is frozen mid-step every time the oven timer goes off. You can't just keep going – you have to write down exactly where you left off, then pick up again later. That's CPS transformation. Jenkins pipelines need to be paused and resumed (e.g., waiting for user approval), so Groovy code is broken into tiny steps that can be saved and restored.

But this magic comes at a cost. Some things you normally do in programming – like using a for loop with a counter, or reading a file – break because the 'freeze and resume' process doesn't handle them well. So you have to write code in a special way, marking certain methods with @NonCPS to say 'this part doesn't need to be paused, just run it normally.'

Think of it like a relay race: most of the pipeline is a relay where batons (state) must be passed carefully, but some parts are solo sprints where you just run flat out. The trick is knowing which parts need the baton and which don't.

I still remember the 2 AM call: 'The pipeline is stuck at 'Loading shared library' and nobody can deploy.' The Jenkins master had been running for 200 days, and a recent Groovy update had broken our entire CI/CD. The root cause? A JsonSlurper call in a CPS-transformed method that had worked for years suddenly started throwing NotSerializableException. That night, I learned the hard way what CPS transformation really means.

Jenkins Pipeline was introduced in 2016 with the 'Scripted Pipeline' DSL, allowing you to write build logic in Groovy. But Groovy wasn't designed for long-running, resumable workflows. To make it work, Jenkins applies a 'Continuation Passing Style' (CPS) transformation to your code. This rewrites your methods into a state machine that can be serialized and deserialized at every step – enabling the pipeline to survive Jenkins restarts and agent disconnections.

But CPS transformation has deep implications. Standard Groovy patterns – loops, closures, exception handling, even simple string concatenation – can behave unexpectedly or fail entirely. In this article, I'll share the exact patterns I've used in production to avoid 'CPS hell.' We'll cover Groovy basics (def, closures, lists, maps) through a pipeline lens, error handling with try/catch/finally, the @NonCPS annotation and its serialization caveats, Script Console debugging, why you should avoid JsonSlurper and use shell commands instead, and how to structure shared libraries to minimize CPS pain.

By the end, you'll be able to write robust, maintainable Jenkins pipelines that won't wake you up at 2 AM.

Groovy Basics for Pipelines: def, Closures, Lists, Maps

In Jenkins pipelines, def is your go-to for variable declaration. Unlike Java's static typing, Groovy's def uses dynamic typing – but be careful: CPS transformation can cause type inference issues. Always declare variables with def to avoid binding problems.

Lists: Use def list = [] or def list = [1, 2, 3]. CPS handles lists well, but avoid modifying them inside a closure that captures non-serializable state. Example:

``groovy def stages = ['build', 'test', 'deploy'] stages.each { stage -> stage 'Stage: ' + stage } ``

Maps: Use def map = [:] or def map = [key: 'value']. Access with map.key or map['key']. Maps are serializable as long as keys/values are serializable.

Closures: Closures are first-class citizens in Groovy but are tricky in CPS. A closure that captures a variable from the enclosing scope forces that variable to be serializable. Avoid capturing Jenkins.getInstance() or other non-serializable objects. Instead, pass data as parameters:

``groovy def processData(data, Closure callback) { callback(data) } // Call with a lambda processData('test') { d -> echo d } ``

Production insight: I once had a closure that captured a Jenkins instance variable. The pipeline would fail intermittently with serialization errors. The fix was to pass the Jenkins URL as a string instead of the object.

Avoid Capturing Non-Serializable Objects in Closures
If a closure references a variable like def jenkins = Jenkins.getInstance(), the closure becomes non-serializable. Always pass simple types (String, int) into closures.
Production Insight
I once had a closure that captured a Jenkins instance variable. The pipeline would fail intermittently with serialization errors. The fix was to pass the Jenkins URL as a string instead of the object.
Key Takeaway
Use def for all variables, prefer each loops, and never capture non-serializable objects in closures.

Try/Catch/Finally Error Handling in Scripted Pipeline

Scripted Pipeline supports try/catch/finally blocks, but CPS transformation changes how they work. A try/catch inside a CPS method can catch exceptions thrown by pipeline steps, but it cannot catch exceptions thrown inside a @NonCPS method – those propagate directly and may bypass serialization.

Pattern: Always wrap pipeline steps in try/catch/finally to ensure cleanup:

``groovy try { sh 'make build' } catch (Exception e) { currentBuild.result = 'FAILURE' echo "Build failed: ${e.message}" throw e // rethrow to fail the pipeline } finally { archiveArtifacts artifacts: 'build/*.zip' } ``

Gotcha: If you have a @NonCPS method that throws an exception, it won't be caught by a CPS try/catch if the method is called inside the try block? Actually, it will be caught, but the stack trace will be mangled. More importantly, if the @NonCPS method uses throw with a non-serializable exception, serialization fails.

Best practice: Keep @NonCPS methods pure (no I/O, no pipeline steps) and let them return success/failure flags rather than throwing exceptions. Use a wrapper method that is CPS to handle exceptions:

```groovy @NonCPS def parseData(String data) { if (data == null) throw new IllegalArgumentException('data cannot be null') // parse return result }

def safeParse(String data) { try { return parseData(data) } catch (IllegalArgumentException e) { echo 'Parse failed: ' + e.message return null } } ```

Re-throw Exceptions to Fail the Pipeline
Catching an exception without rethrowing will mark the stage as green. Always call error(...) or rethrow to propagate failure.
Production Insight
We once had a finally block that called emailext to notify on failure. Because the pipeline had already failed, the finally block ran but the email step threw another exception, masking the original error. We added a nested try/catch around email.
Key Takeaway
Always use try/catch/finally around pipeline steps, but keep @NonCPS methods exception-safe.

CPS Transformation: How It Works and Its Limitations

CPS transformation is the magic that makes pipelines resumable. When Jenkins compiles your Groovy script, it uses the workflow-cps plugin to rewrite the code into a state machine. Each 'step' (any method that can suspend, like sh, input, sleep) becomes a checkpoint. The entire program state (local variables, call stack) is serialized to disk.

Limitations: 1. Loops: Only for (item in iterable), while, and list.each {} are supported. Classic for (int i=0; i<n; i++) is not because the CPS transformer can't handle the mutable integer i as a continuation variable. 2. Switch statements: Not supported. Use if/else if chains. 3. Closures: Must be serializable. Avoid capturing non-serializable objects. 4. Recursion: Can cause stack overflow because each recursive call creates a new continuation. 5. Threading: No Thread.start() or parallel execution without parallel step.

How to check if a method is CPS: If it calls any pipeline step (like sh, echo, stage), it's CPS. If it's annotated with @NonCPS, it's not.

Debugging CPS: Use Script Console to inspect the transformed code? Not directly, but you can run println 'Hello' and see if it prints – if it doesn't, CPS may have swallowed the output. Use echo instead of println in pipelines.

CPS Transformation Is Not Perfect
If you hit a limitation, refactor the code into a @NonCPS helper method that does the heavy lifting and returns a simple result.
Production Insight
I had a pipeline that used for (int i=0; i<3; i++) to retry a build. It worked for months until a Jenkins upgrade changed the CPS transformer behavior, causing MissingMethodException. We changed to (1..3).each { attempt -> ... } and it worked.
Key Takeaway
Understand that CPS transforms your code into a state machine; avoid unsupported constructs and keep CPS methods simple.

Using @NonCPS Annotation Correctly

The @NonCPS annotation tells Jenkins to run the method without CPS transformation. This means the method runs atomically – it cannot be paused or resumed. Use it for: - Data processing (parsing JSON, XML, regex) - Pure computation (sorting, filtering) - Any code that doesn't need to call pipeline steps

Important: Inside a @NonCPS method, you cannot use sh, echo, stage, or any pipeline step. If you try, you'll get a MissingMethodException or a serialization error.

Serialization: Even though the method isn't CPS-transformed, its arguments and return value must be serializable because the caller's CPS context will serialize them at the next checkpoint. For example:

``groovy @NonCPS def process(List<String> items) { return items.collect { it.toUpperCase() } } ` Here, List<String>` is serializable, so it's safe.

Common mistake: Annotating a method that calls echo – this will fail. Instead, structure your code so that @NonCPS methods are pure, and CPS methods call them:

```groovy def myPipelineStep() { def raw = sh(script: 'cat data.json', returnStdout: true) def processed = processData(raw) // @NonCPS echo "Processed: $processed" }

@NonCPS def processData(String raw) { // parse with regex or simple string manipulation return raw.split(' ').find { it.contains('key') } } ```

Never Use Pipeline Steps Inside @NonCPS
If you need to call sh or echo, move that to the caller CPS method. @NonCPS methods must be pure functions.
Production Insight
I once annotated a method with @NonCPS that called sh 'some command'. The pipeline compiled but failed at runtime with java.lang.IllegalStateException: Cannot call method sh on a non-CPS method. We refactored to separate the shell call from the processing.
Key Takeaway
Use @NonCPS for pure data processing that doesn't need pipeline steps, and ensure arguments/return values are serializable.

Serialization Considerations in Jenkins Groovy

Serialization is the backbone of pipeline resumability. Every time the pipeline reaches a checkpoint (e.g., after a sh step), Jenkins serializes the entire CPS execution state – including all local variables, the call stack, and any objects referenced by closures. If any object in this graph is not serializable, the pipeline crashes with NotSerializableException.

What is serializable? - Primitive types (int, boolean, etc.) - String, Number, Date - Arrays and Collections (List, Map, Set) of serializable elements - Classes that implement java.io.Serializable - Groovy's GString (but avoid using it as a variable – use String instead)

What is NOT serializable? - groovy.json.JsonSlurper, groovy.xml.XmlSlurper - Jenkins, Jenkins.getInstance() - java.io.InputStream, java.io.OutputStream - Custom classes without Serializable

Pattern: Minimize the scope of non-serializable objects. For example, if you need to read a file, open the stream, read the data, and close it all within a single @NonCPS method:

``groovy @NonCPS def readFileContent(String path) { def file = new File(path) def content = file.text return content } ` But be careful: File is serializable? Actually, java.io.File implements Serializable, so it's safe. But FileReader` is not.

Debugging serialization: Use Script Console to test if an object is serializable:

``groovy def obj = [1, 2, 3] println obj instanceof Serializable // true ``

Use Script Console to Check Serialization
Run println myObject instanceof Serializable to quickly test if an object will cause issues.
Production Insight
We had a pipeline that stored a HashMap<String, Object> where the value was a File object. The File was serializable, but the HashMap itself was not because of a custom class loader. We changed the value to a String path.
Key Takeaway
Always ensure all objects in pipeline variable scope are serializable, or use @NonCPS to limit their lifetime.

Script Console: Your Best Debugging Tool

The Jenkins Script Console at http://jenkins:8080/script allows you to run arbitrary Groovy code on the master. It's invaluable for debugging pipelines because you can test snippets without running a full pipeline.

Common uses
  • Check Groovy version: println GroovySystem.version
  • Test serialization: println [1,2,3] instanceof Serializable
  • Inspect Jenkins state: println Jenkins.instance.pluginManager.plugins
  • Approve blocked methods: hudson.model.UsageStatistics.get()?.doPostCheck() (but normally you use the UI)

Warning: Script Console runs with full permissions. Be careful not to run destructive commands like System.exit(1).

Debugging a specific pipeline: You can get the current build's CPS execution state:

``groovy def job = Jenkins.instance.getItemByFullName('my-job') def build = job.getBuildByNumber(123) println build.getExecution()?.serialize() ``

Testing CPS behavior: You can't directly simulate CPS in Script Console, but you can write a small test:

``groovy import org.jenkinsci.plugins.workflow.cps.CpsScript // Not directly usable, but you can test if a class is serializable ``

Script Console Is a Double-Edged Sword
It's powerful for debugging but also a security risk. Restrict access to admins only.
Production Insight
I once used Script Console to inspect a stuck pipeline's execution state and found a non-serializable HashMap entry. The console saved hours of trial and error.
Key Takeaway
Use Script Console to test serialization, check versions, and inspect pipeline state – it's the fastest debug tool.

Why You Should Avoid JsonSlurper and XmlSlurper (Use Shell Instead)

JsonSlurper and XmlSlurper are convenient Groovy classes for parsing structured data, but they are NOT serializable. Using them inside a CPS-transformed pipeline method will cause NotSerializableException at the next checkpoint.

Alternative: Use shell commands with jq for JSON and xmllint for XML. These tools are fast, reliable, and don't require serialization because the pipeline only sees the output string.

Example: Parsing JSON:

``groovy def json = sh(script: 'echo \'{"name":"test"}\' | jq -r \'.name\'', returnStdout: true).trim() ``

Example: Parsing XML:

``groovy def value = sh(script: 'echo \'<root><element>value</element></root>\' | xmllint --xpath \'string(//element/text())\' -', returnStdout: true).trim() ``

Handling complex data: If you need to extract multiple fields, write a shell script that outputs a simple format (e.g., key=value lines) and parse that in Groovy:

``groovy def output = sh(script: 'jq -r \'to_entries[] | "\(.key)=\(.value)"\' data.json', returnStdout: true) def map = [:] output.readLines().each { line -> def parts = line.split('=', 2) map[parts[0]] = parts[1] } ``

Performance: Shell parsing is often faster than Groovy parsing for large files, and it avoids serialization issues entirely.

Never Use JsonSlurper in Pipeline Code
It will cause NotSerializableException. Always delegate to jq or xmllint via sh step.
Production Insight
After the incident where JsonSlurper crashed our pipeline, we replaced all JSON parsing with jq. The pipelines became more reliable and faster.
Key Takeaway
Use shell tools (jq, xmllint) instead of JsonSlurper/XmlSlurper to avoid serialization errors.

Shared Library Patterns in Groovy

Shared libraries allow you to reuse pipeline logic across multiple repositories. The library is stored in a Git repo and loaded with library 'my-lib@branch'. The Groovy files in vars/ are automatically loaded as global variables and are CPS-transformed.

Structure: `` (root) +- vars/ | +- myStep.groovy // defines a function myStep | +- myMethod.groovy // defines a function myMethod +- src/ +- org/my/lib/ +- MyHelper.groovy // helper class, not CPS-transformed ``

Best practices: 1. Keep vars/ methods thin – they should call helper classes for heavy logic. 2. Use @NonCPS in src/ classes for data processing. 3. Avoid storing state in global variables – use method parameters. 4. Use library 'my-lib@v1.2.3' to pin versions.

Example:

```groovy // vars/buildApp.groovy def call(String project, String branch) { def helper = new org.my.lib.BuildHelper() def config = helper.loadConfig(project) stage('Build') { sh "make -C ${config.dir}" } }

// src/org/my/lib/BuildHelper.groovy @NonCPS def loadConfig(String project) { def config = [:] // read from file or API return config } ```

Gotcha: If you use return in a vars/ method, the return value must be serializable. If you return a complex object, ensure it implements Serializable.

Testing: You can test shared library code locally by running groovy command-line, but you'll need to mock Jenkins steps.

Pin Shared Library Versions
Use library 'my-lib@v1.2.3' to avoid unexpected changes breaking your pipelines.
Production Insight
We had a shared library that loaded configuration from a YAML file using org.yaml.snakeyaml. The Yaml object is not serializable, causing random failures. We moved the YAML parsing to a @NonCPS helper and passed the result as a map.
Key Takeaway
Structure shared libraries with thin CPS-transformed vars/ methods and thick @NonCPS helper classes for heavy lifting.

Advanced Error Handling: Retry and Timeout Patterns

Jenkins provides built-in steps retry and timeout. Combine them with try/catch for robust error handling.

Retry with backoff: Jenkins doesn't have built-in exponential backoff, but you can implement it:

``groovy def retryWithBackoff(int maxRetries, int initialDelay = 10, Closure body) { for (int attempt = 1; attempt <= maxRetries; attempt++) { try { body() return } catch (Exception e) { if (attempt == maxRetries) throw e echo "Attempt $attempt failed, retrying in ${initialDelay attempt} seconds" sleep time: initialDelay attempt, unit: 'SECONDS' } } } ``

Timeout: Use timeout to prevent hung builds:

``groovy timeout(time: 30, unit: 'MINUTES') { sh 'make test' } ``

Combined:

``groovy timeout(time: 1, unit: 'HOURS') { retry(3) { sh 'deploy' } } ``

Gotcha: retry step internally catches exceptions and retries, but if the exception is not serializable, it can fail. Ensure your body doesn't throw non-serializable exceptions.

Production insight: We had a pipeline that retried a database migration. The migration threw a custom exception that wasn't serializable, causing the retry to fail with serialization error. We changed to catch and rethrow a RuntimeException.

Use Built-in Steps When Possible
retry and timeout are optimized for CPS. Implement custom retry only when you need backoff.
Production Insight
We had a pipeline that retried a database migration. The migration threw a custom exception that wasn't serializable, causing the retry to fail with serialization error. We changed to catch and rethrow a RuntimeException.
Key Takeaway
Combine retry, timeout, and try/catch for robust error handling, but ensure exceptions are serializable.

Parallel Execution and CPS: Pitfalls and Patterns

The parallel step allows running stages concurrently. However, each branch runs in its own CPS context, which can lead to serialization issues if they share non-serializable objects.

Pattern:

``groovy parallel( branch1: { stage('Build A') { sh 'make a' } }, branch2: { stage('Build B') { sh 'make b' } } ) ``

Gotcha: If you define variables outside the parallel block and modify them inside, the changes won't be visible because each branch has isolated state. Use return to collect results:

``groovy def results = [:] parallel( a: { results.a = sh(script: 'echo result_a', returnStdout: true).trim() }, b: { results.b = sh(script: 'echo result_b', returnStdout: true).trim() } ) echo "Results: $results" ``

Serialization: The results map must be serializable. If you use a non-serializable object, parallel will fail.

Production insight: We once used parallel to run tests on multiple agents. One agent had a different environment that caused a non-serializable object to be captured in a closure, crashing the entire parallel block. We isolated the environment-specific logic into separate stages.

Parallel Branches Have Isolated State
Modifying a shared variable inside a parallel branch does not affect the outer scope. Use return values or a shared serializable map.
Production Insight
We once used parallel to run tests on multiple agents. One agent had a different environment that caused a non-serializable object to be captured in a closure, crashing the entire parallel block. We isolated the environment-specific logic into separate stages.
Key Takeaway
Use parallel with care, ensure shared data is serializable, and collect results via return values.

Debugging with println vs echo and Script Console

In Jenkins pipelines, echo is the recommended way to print messages because it goes through the pipeline logger and is serializable. println writes to System.out, which may not be captured in the build log and can cause serialization issues if used inside a CPS method.

Rule: Always use echo in pipeline scripts. Use println only in Script Console or in @NonCPS methods (but even then, prefer echo via a helper).

Script Console debugging: You can test serialization of objects:

``groovy def testObj = [1,2,3] println testObj instanceof Serializable ``

Inspecting pipeline variables: If you have a pipeline that fails, you can attach the Script Console to a running build? Not directly, but you can inspect the build's CPS state after failure:

``groovy def job = Jenkins.instance.getItemByFullName('my-job') def build = job.getBuildByNumber(123) def exec = build.getExecution() if (exec) { println exec.serialize() } ``

Production insight: I once spent hours trying to debug a pipeline that printed nothing with println. Switching to echo revealed the issue immediately.

Use echo, Not println
echo is pipeline-aware and serializable; println can cause issues and may not appear in logs.
Production Insight
I once spent hours trying to debug a pipeline that printed nothing with println. Switching to echo revealed the issue immediately.
Key Takeaway
Always use echo for logging in pipelines; use Script Console for interactive debugging.

Common Groovy Pitfalls: GString, null, and Type Coercion

Groovy's dynamic nature can lead to subtle bugs in pipelines.

GString: "Hello $name" creates a GString object, which is serializable but can cause issues if you use it as a map key. Always convert to String if needed: "Hello $name".toString().

Null safety: Groovy's ?. operator (safe navigation) is safe in CPS. Use it to avoid null pointer exceptions:

``groovy def value = map?.key?.nested ``

Type coercion: Groovy automatically converts types, which can lead to unexpected behavior. For example, if (list) returns true if list is non-empty. Be explicit: if (list != null && !list.isEmpty()).

== vs equals: In Groovy, == calls equals() by default. For identity comparison, use is(). This is fine in CPS.

Production insight: We had a bug where a map key was a GString, and later lookup with a String key returned null. We fixed by always using .toString() on keys.

Convert GString to String for Map Keys
GString and String are not equal in map lookups. Always use key.toString() when inserting into a map.
Production Insight
We had a bug where a map key was a GString, and later lookup with a String key returned null. We fixed by always using .toString() on keys.
Key Takeaway
Be explicit with types, convert GStrings, and use safe navigation to avoid null issues.
● Production incidentPOST-MORTEMseverity: high

The JsonSlurper Serialization Meltdown

Symptom
Pipeline failed with java.io.NotSerializableException: groovy.json.JsonSlurper at the line def json = new JsonSlurper().parseText(text)
Assumption
We assumed JsonSlurper was serializable because it's a Groovy standard class.
Root cause
JsonSlurper is not serializable, and the CPS transformation tried to serialize the entire method's local variables (including the JsonSlurper instance) at a checkpoint.
Fix
Replaced JsonSlurper with a shell command: def result = sh(script: 'echo \'$JSON\' | jq -r \'.key\'', returnStdout: true).trim()
Key lesson
  • Any object that isn't serializable will cause CPS serialization errors.
  • Use @NonCPS for pure data processing or avoid non-serializable classes entirely by delegating to shell tools.
Production debug guideSymptom → Root cause → Fix4 entries
Symptom · 01
Pipeline fails with java.io.NotSerializableException at a random line
Fix
Root cause: A variable in scope is not serializable. Fix: Wrap the code in a @NonCPS method or ensure all objects in scope implement Serializable. Use Script Console to test serialization: println(Thread.currentThread().contextClassLoader.loadClass('org.jenkinsci.plugins.workflow.cps.CpsVmThread').current.getExecution().serialize())
Symptom · 02
Groovy method with for loop throws groovy.lang.MissingMethodException: No signature of method: java.util.ArrayList.each()
Fix
Root cause: CPS transformation rewrites loops into closures, but some loop constructs are not supported. Fix: Use for (item in list) or list.each { } – avoid for (int i=0; i<list.size(); i++) style loops.
Symptom · 03
Script Console shows Scripts not permitted to use method groovy.json.JsonSlurper
Fix
Root cause: Jenkins script security plugin blocks dangerous methods. Fix: Use sh 'jq ...' instead, or approve the method in Manage Jenkins → In-process Script Approval.
Symptom · 04
Shared library method returns null but works locally
Fix
Root cause: CPS-transformed method returns a value that is not serializable and gets lost. Fix: Ensure the return type is serializable (e.g., String, int, List of Serializable). Use @NonCPS on the method if it just processes data.
★ Jenkins Groovy Scripting for Pipelines Quick Referenceprint this for your desk
NotSerializableException
Immediate action
Identify the non-serializable object
Commands
println(Thread.currentThread().contextClassLoader.loadClass('org.jenkinsci.plugins.workflow.cps.CpsVmThread').current.getExecution().serialize())
Check which variable is non-serializable by commenting out code
Fix now
Wrap offending code in @NonCPS method
MissingMethodException on loop+
Immediate action
Check loop syntax
Commands
print 'Loop type: ' + (list instanceof Iterable)
Fix now
Use for (item in list) or list.each { }
JsonSlurper/XMLSlurper error+
Immediate action
Replace with shell command
Commands
sh 'echo \'$JSON\' | jq -r \'.key\''
sh 'echo \'$XML\' | xmllint --xpath \'//element/text()\' -'
Fix now
Remove JsonSlurper; use jq/xmllint
Shared library method returns unexpected null+
Immediate action
Check return type serialization
Commands
println 'return type: ' + result.getClass().name
Fix now
Annotate method with @NonCPS if it doesn't use pipeline steps
Script Console approval error+
Immediate action
Approve the method or use alternative
Commands
Open Manage Jenkins → In-process Script Approval
Search for the method signature and approve
Fix now
Use shell commands instead of blocked methods
CPS vs @NonCPS: When to Use Each
CharacteristicCPS Method@NonCPS Method
Can call pipeline steps (sh, echo, stage)YesNo
Supports resumability (pause/suspend)YesNo
Serialization of local variablesRequired at checkpointsNot required (but arguments/return must be serializable)
Loop supportLimited (for-in, each, while)Full Java/Groovy loops
Exception handlingCPS-aware try/catchStandard try/catch (but can't catch pipeline step errors)
PerformanceSlower due to state machine overheadFaster, no overhead
Use casePipeline orchestration, stepsData processing, parsing, pure computation
📦 Downloadable Quick Reference

Print-friendly master reference covering all topics in this track.

⇩ Download PDF

Key takeaways

1
Always use def for variables, for (item in list) for loops, and echo for logging.
2
Wrap pipeline steps in try/catch/finally and rethrow exceptions to fail the build.
3
Use @NonCPS for pure data processing methods that don't call pipeline steps.
4
Ensure all objects in pipeline variable scope are serializable; use Script Console to test.
5
Avoid JsonSlurper/XmlSlurper
use sh 'jq' or sh 'xmllint' instead.
6
Structure shared libraries with thin vars/ methods and @NonCPS helper classes in src/.
7
Pin shared library versions to prevent unexpected changes.
8
Use retry and timeout built-in steps for robust error handling.

Common mistakes to avoid

6 patterns
×

Using `for (int i=0; i<n; i++)` in a pipeline

Symptom
MissingMethodException or weird behavior
Fix
Use for (item in list) or list.each { }
×

Storing `Jenkins.getInstance()` in a variable

Symptom
NotSerializableException at checkpoint
Fix
Pass the Jenkins URL as a string instead
×

Using `println` instead of `echo`

Symptom
Messages not appearing in build log or serialization errors
Fix
Replace all println with echo
×

Annotating a method with @NonCPS that calls `sh`

Symptom
IllegalStateException: Cannot call method sh on a non-CPS method
Fix
Move the sh call to a CPS method, keep @NonCPS for pure logic
×

Using `JsonSlurper` inside a pipeline

Symptom
NotSerializableException: groovy.json.JsonSlurper
Fix
Use sh 'jq ...' to parse JSON
×

Not pinning shared library version

Symptom
Pipeline behavior changes unexpectedly after library update
Fix
Use library 'my-lib@v1.2.3' with a specific tag
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is CPS transformation in Jenkins Pipeline?
Q02SENIOR
When should you use the @NonCPS annotation?
Q03SENIOR
Why does `for (int i=0; i<10; i++)` fail in a Jenkins pipeline?
Q04SENIOR
How do you parse JSON in a Jenkins pipeline without causing serializatio...
Q05JUNIOR
What is the difference between `echo` and `println` in a pipeline?
Q06SENIOR
How do you structure a shared library to avoid CPS issues?
Q07SENIOR
Can you catch exceptions thrown inside a @NonCPS method from a CPS try/c...
Q08JUNIOR
What is the Script Console and how do you use it for debugging?
Q01 of 08SENIOR

What is CPS transformation in Jenkins Pipeline?

ANSWER
CPS (Continuation Passing Style) transformation is a process where Jenkins rewrites Groovy code into a state machine that can be serialized and deserialized at every pipeline step. This allows pipelines to be paused (e.g., waiting for input) and resumed later, even after a Jenkins restart. The transformation is done by the workflow-cps plugin.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
Why does my pipeline fail with NotSerializableException?
02
Can I use Java libraries in Jenkins pipelines?
03
How do I debug a pipeline that hangs?
04
What is the difference between Scripted and Declarative Pipeline?
05
How do I share global variables across pipeline runs?
06
Can I use parallel with @NonCPS methods?
07
Why does my shared library method return null?
08
How do I handle timeouts in a pipeline?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
July 09, 2026
last updated
230
articles · all by Naren
🔥

That's Jenkins. Mark it forged?

9 min read · try the examples if you haven't

Previous
Jenkins Kubernetes Integration
35 / 39 · Jenkins
Next
Jenkins REST API & CLI Automation