Blog
SAP CPQ Scripting: Where the Documentation Splits and What It Leaves Out
The documentation for SAP CPQ scripting is spread across four SAP Help documents that do not always agree. This article maps which script goes where, what fires when, and why code that works in the Script Workbench fails on a real quote.
What you will learn
- Where global, product and custom action scripts are created, and why modules cannot hold state
- Which events exist on Quote 1.0 and Quote 2.0, and the order they fire in when a product is added
- Why Product or Context.Quote is None, and what KBA 3648684 says about it
- The limits, logging rules and 2602 to 2609 changes to check before editing a script
The documentation for SAP CPQ scripting is not missing. It is spread across four documents written at different times, and they do not always use the same names for the same things: the Scripting Events, Properties and Event Pipeline guide, the Setup and Administration Guide for Quote 1.0, the Limitations and Known Issues page, and the Scripting Documentation with the interface reference.
I read them against the 2609 documentation, then went through the SAP knowledge base articles and community threads where people actually get stuck. This is the map I wanted at the start: which script goes where, what the events do, why code that runs in the Script Workbench fails on a real quote, and which limits to design around. Where SAP is silent or the pages disagree, I say so.
SAP CPQ scripting starts with knowing which kind of script you are writing
Global scripts are created under Setup, Develop, Global Scripts. SAP describes a script there as “a series of IronPython commands that can be executed without being compiled”, and every global script has three tabs: Script, Events and History. The create form asks for a Rule Name, a System ID that is generated for you, Active, Module, CSRF Protection and Force Proxy Generation. The last one loads or refreshes WSDL web services and only needs ticking once, on the first save.
Product scripts live in the product itself, under Setup, Product Catalog, Products, on the Scripts tab. They come in two kinds: local ones that run for that product only, and global ones that can be attached to several products. Custom actions are the third place, under Workflow/Approvals Actions, where a script can run before or after an action. Two details are easy to miss there. SAP states that “if a custom action causes and error, the system stops the workflow execution”, and a script that is supposed to change the quote status in a workflow transition should have WorkflowContext.ChangeQuoteStatus = True as its last line. If you only need the vocabulary, our glossary has short definitions of a global script and IronPython.
Ticking Module turns a global script into a library. It can no longer be used on its own or attached to an event, only imported by other scripts with import TestModule or from TestModule import Add. SAP’s warning reads: “Do not store the context, or any other state, in scripting modules. The system caches modules, so changing the context invalidates the cached reference.”
Inside a module only stateless objects are available: ScriptExecutor, Log, Trace, SqlHelper, XmlHelper, RestClient, mTLSRestClient, UserPersonalizationHelper, FileHelper and Translation. Product, Quote and SFEnvironment have to arrive as function parameters.
A 2021 community thread asks why importing the reportlab module in a global script throws “No module named reportlab”. It is not a bug. The Scripting General Guidelines say that “the system does not support standard IronPython modules” and that you cannot add external .NET and complex IronPython modules. Simple modules without dependencies can be pasted into a script module, and that is the only exception I found. Internal modules, described above, are a different thing.
The .NET side is restricted too. System.IO, System.Data, System.Configuration, System.Environment, System.GC, System.Type, System.Web.WebPages.Razor, System.Reflection, System.Web and System.Diagnostics are forbidden, and a script that contains “System import *” plus one of those names cannot be saved. Since the 2402 release the CLR module import is prohibited outright. SAP publishes replacements: RestClient instead of HttpClient and WebRequest, JsonHelper instead of Newtonsoft.Json, XmlHelper instead of System.Xml, list comprehensions instead of System.Linq. One more thing I could not find on any help page I read: the IronPython version. I would not write a script on assumptions about Python 2 or Python 3 syntax.

Events: where the pages disagree and what fires when
Events are the part of SAP CPQ scripting that SAP documents across the most pages. The Global Script Events table in the Quote 1.0 administration guide has a column per engine, and it matters. “Before custom fields calculation”, “After custom fields calculation” and “Every time quote is changed” are available on Quote 1.0 only. “Before adding products to quote”, “Before updating products in quote”, “On Involved Parties Changed”, “On Market Change”, “On Pricebook Change” and “On Effective Change” exist on Quote 2.0 only. A script copied from an older tenant can have nothing to attach to.
Two behaviors from the same table are worth remembering. After adding several products at once, the scripts run once, after the last item, not after each one. And “each change of market also changes the pricebook, but the event On Pricebook Change is not triggered”.
The order when a product is added
For a simple product on Quote 2.0, SAP lists this pipeline: OnQuoteCreated (only if a quote does not exist yet), OnProductRuleExecutionStart, OnProductRuleExecutionEnd, OnProductLoaded, OnProductReady, OnProductBeforeAddToQuote, BeforeItemAdded, OnItemAdded and BeforeQuoteRendering. For a configurable product the order changes. The rule execution events and OnProductLoaded and OnProductReady come first, the rules run again for every attribute selection, and only then does OnQuoteCreated fire, followed by OnProductBeforeAddToQuote, BeforeItemAdded and AfterItemAdded, or BeforeItemUpdated and AfterItemUpdated when an existing item is updated.
The practical consequence is that a script on OnQuoteCreated runs first for a simple product and only after the whole configuration for a configurable one. Our piece on why an event on quote creation can run against an empty quote covers what that means when quotes are created through the API.
Do not assume this is one list. The simple product page names OnItemAdded, the configurable product page names BeforeItemAdded and AfterItemAdded, and the Iron Python Events page lists none of BeforeItemAdded, AfterItemAdded, BeforeItemUpdated, AfterItemUpdated, OnProductReady or BeforeQuoteRendering.
Events that do not fire when you expect them to
Container events (onRowAdded, onRowCopied, onRowDeleted, onCellChanged) trigger only on user interaction. If another script manipulates the container, they stay silent. OnCustomFieldChanged depends on an application parameter, “Fire event – Quote custom field change – only on changes performed by user”: when it is true the event follows manual edits only, and when it is false it also fires for changes made by scripting.
SAP’s note on rules reads: “rules will not be triggered when attribute values are modified through scripting”, so add Product.ApplyRules() before updating a configuration if you want the standard rules to run. OnProductAddedToQuote and OnProductBeforeAddToQuote run for the root product only, not for children, and calling Quote.Save() with the parameter set to False suppresses quote creation, the custom field calculation events, “Every time quote is changed” and “When user is on quotation tab”.
Why a script works in the Script Workbench and fails on the quote
The object that is None
SAP’s own knowledge base confirms this in KBA 3648684: the same script “may execute successfully in Script Workbench” and still fail with System.MissingMemberException: ‘NoneType’ object has no attribute ‘PartNumber’. The cause is that on the Q2.0 engine the Product object is not supported in the Pre Action of the Add Item action. SAP’s advice is to use the product-level event On Before Add To Quote instead. Other messages that point away from their cause are listed in this layer-by-layer guide to SAP CPQ error messages, together with the screen to open first.
The same shape of problem appears in a February 2026 community thread. A global script that touched Product.Attributes was attached to a quote-level event, where, as the accepted answer put it, you work with a quote item and not with the product definition object. The fix was to detach it and bind the script to product events. A third variant comes with parallel quoting: KBA 3753301 explains that context.Quote can be None in the Script Workbench when no quote is in the session, and KBA 3768286 lists Context.Quote missing in SYSTEM_CPQ_QuoteFieldAccessLevel as an application bug with a fix tentatively planned for 2608. Check the What’s New viewer before relying on that. SAP’s documentation fits the pattern: Quote is “available only when the quote is loaded”, and in the Workbench’s Test mode many API objects, the quote among them, are not available. Our list of scripting limits and the Developer Console helps when you need to see which context a failing script runs in.
State that outlives the script
KBA 3750913 covers “Cannot perform runtime binding on a null reference”. The cause can be static variables inside classes in IronPython scripts combined with internal script caching, so values set earlier survive between executions. SAP’s fix is to replace class-level static variables with a method that calculates the value at runtime.
The other classic is QuoteAlreadyModified. KBA 3751407 describes a script that updates quote data in memory while a CPQ API call or an external process updates the same quote within the same execution, and it sends you to a support case and a review of the script logic. A September 2025 thread reports Msg.QuoteAlreadyModified when a script runs from a custom field change or before quote rendering, and it ends without a solution. Since 2608 the error at least shows up in the execution logs.
A 2022 Quote 2.0 thread shows “can’t assign to function call” for context.Quote.GetCustomField(“ApprovalModDateCompliance”) = FormatedTimestamp. The field object is not the value: assign to its .Value property, and a date field wants a DateTime, not a string. The working version imports DateTime from System and sets .Value = DateTime.Now. The second trap is arithmetic. A SAP Community blog from September 2024 recommends writing 2.0/3 instead of 2/3 in the Workbench, because “otherwise, you receive 0 as your result”.

Limits, logging and what to check before you touch a script
The limits decide how you structure scripts. One line of IronPython may contain at most 50 operators, numeric and string alike, and a request may not nest more than 10 script calls: domains created before 2202 get a warning in the log, newer ones get an error. Timeouts are where the sources split. The Script Performance page for 2609 states 900 seconds for existing tenants and 300 seconds for new ones, while a 2022 SAP Community blog speaks of 20 minutes, so check which applies to your tenant. The Script Workbench itself stops at 3 minutes. The same blog introduced a 40,000 character limit per script with the 2205 release, which I could not confirm on the current help pages.
The Limitations and Known Issues page for 2609 adds three more: SqlHelper.GetList returns at most 1000 records, an API request from SAP CPQ to an external system times out after 100 seconds, and a Quote 2.0 section calculation only sees the fields of the current section row. For the slow scripts those limits eventually expose, see how slow scripts show up under Script Performance.
Trace, Log and the Developer Console
Trace.Write is for debugging and writes to the Developer Console. Log.Write is for values you want to remember “forever”, and logs are slower to process than traces. There is one exception: in a post action of Generate Documents, Trace.Write is not recorded in the Developer Console, so use Log.Write there.
The console captures up to 100,000 traces per request, where a request is a click in the application, and it can download them as an XLSX. Its “Disable Iron Python Script Execution” option does what it says: while the console is open, neither global nor local scripts run.
Recent changes worth a script inventory
As of 2602, the IronPython eval() statement is deprecated. Existing scripts keep working, but SAP says you will not be able to modify them unless you remove it, and admins were asking about exactly this in a community thread in summer 2025. If you have not listed which scripts still use it, the inventory of scripts that rely on eval() in our 2602 preparation guide is where I would start. The 2605 release added standard approval actions callable from a script (Submit for Approval, Retract Approval Process, Approve, Reject) and a Reprice action through the WorkflowExecutor object, which is the background to retracting an approval from a script. The VS Code plugin supports only global scripts. Release 2608 fixed the “No Module Found” import error in modules, and the next deprecation on the list is the HttpWebClientProtocol interface, which SAP announced in 2605 and will remove in 2702, so scripts that call it need to move to RestClient. Release 2611 pulls in the other direction and adds configuration and parent product access to scripting.
Before changing a script on a tenant you did not set up, three questions save most of the pain: which engine and which event is it attached to, which objects exist in that context, and what do the 2609 pages say today. SAP CPQ scripting goes much smoother once that habit is in place.