SfSkills Domains Docs GitHub

Home / apex / cpq-apex-plugins

cpq-apex-plugins

Implementing Salesforce CPQ plugin interfaces in Apex or JavaScript (JS QCP) to customize quote calculation, product search, order creation, contracting, or configuration screens.

Open the skill folder on GitHub


CPQ Apex Plugins

This skill covers implementing Salesforce CPQ plugin interfaces — both Apex-based and JavaScript Quote Calculator Plugin (JS QCP) — that let practitioners intercept and customize CPQ's calculation engine, configurator, order creation, and contracting flows. Activate when a business requirement cannot be met by declarative CPQ features alone and a coded plugin hook is required.


Before Starting

Gather this context before working on anything in this domain:


Core Concepts

Plugin Type Taxonomy

CPQ exposes nine distinct plugin interfaces. Each targets a different extension point:

Plugin InterfaceApex NamespacePrimary Purpose
QuoteCalculatorPluginSBQQOverride quote line pricing and totals (legacy — prefer JS QCP)
JS QCP (SBQQ__CustomScript__c)JavaScriptModern calculator hook with 7 lifecycle events
ProductSearchPluginSBQQFilter or reorder product catalog search results
QuoteTermPluginSBQQGenerate or transform quote terms before finalization
ProductRulePluginSBQQExtend product rule evaluation logic
ConfigurationAttributePluginSBQQCustom behavior for configuration attribute changes
ConfigurationInitializerPluginSBQQPre-populate options before configurator screen loads
OrderPluginSBQQHook into order creation from a quote
ContractingPluginSBQQHook into contract creation from an order

The CpqPlugin interface is the base that all Apex plugins extend — never implement it directly.

JS QCP vs. Legacy Apex QuoteCalculatorPlugin

The JS QCP (SBQQ__CustomScript__c) is the current supported approach for calculator customization. It stores JavaScript code in the SBQQ__Code__c field of an SBQQ__CustomScript__c record. The record must have SBQQ__Active__c = true and be associated with a quote template or set as the default script.

JS QCP exposes seven named hooks, each receiving (quoteModel, quoteLineModels, conn):

  1. onInit — fires when the calculator initializes; use to set default field values
  2. onBeforeCalculate — fires before the calculation engine runs; use to pre-process line fields
  3. onAfterCalculate — fires after calculation; use to post-process totals
  4. onBeforePriceRules — fires before CPQ evaluates price rules
  5. onAfterPriceRules — fires after price rules complete
  6. onBeforeCalculatePrices — fires before each pricing pass
  7. onAfterCalculatePrices — fires after each pricing pass

Each hook must return a resolved Promise. Returning nothing or a non-Promise causes the calculation to hang indefinitely.

The legacy Apex QuoteCalculatorPlugin implements SBQQ.QuoteCalculatorPlugin with calculate(quoteLines, callback). It is still functional but not recommended for new development. It cannot coexist with an active JS QCP.

Plugin Registration

Each plugin type has a dedicated registration field on the CPQ Settings record (SBQQ__CustomActionSettings__c or the CPQ package settings object). The Apex class name (fully qualified, no namespace prefix for the developer's own code) is entered in the corresponding field:

For the JS QCP, no Settings field is used — the active SBQQ__CustomScript__c record drives activation.

Async Calculations with CalculateCallback

Apex plugin methods that need to perform callouts or DML before returning results must use the SBQQ.CalculateCallback interface. The plugin's calculate method receives a callback object; the implementation invokes callback.run(quoteLines) to signal completion. This prevents synchronous blocking of the CPQ engine but adds complexity — errors thrown after callback.run() do not surface cleanly.


Common Patterns

Pattern 1: JS QCP for Custom Pricing Logic

When to use: The business requires pricing logic that reads external data (via JS fetch to a Salesforce endpoint), evaluates complex conditional pricing not expressible in price rules, or needs access to quote-level aggregates mid-calculation.

How it works:

  1. Author a JavaScript object with the required hook functions exported as named exports.
  2. Store the code in SBQQ__CustomScript__c.SBQQ__Code__c. Set SBQQ__Active__c = true.
  3. Each hook receives quoteModel (the quote record + fields) and quoteLineModels (array of quote line records + fields).
  4. Return a resolved Promise from each hook, even if no changes are made (return Promise.resolve()).
  5. To modify a line, mutate properties on the quoteLineModels entries; CPQ reads them back after the hook resolves.

Why not Apex triggers: Apex triggers on SBQQ__QuoteLine__c fire outside the CPQ calculation transaction. CPQ recalculates on save and overwrites any values set by triggers during the calculation pass, creating an infinite loop or silent data loss.

Pattern 2: Apex OrderPlugin for Custom Order Line Logic

When to use: The business requires Apex logic (DML on custom objects, platform events, callouts) to run immediately when CPQ converts a quote to an order. No JS equivalent exists for OrderPlugin.

How it works:

  1. Create an Apex class implementing SBQQ.OrderPlugin.
  2. Override onBeforeInsert(List<Order> orders, SBQQ.DefaultOrderProduct defaultOrderProduct, Database.UnitOfWork uow) and/or onAfterInsert.
  3. Use the provided uow (Unit of Work) to register new records. Do not perform DML directly — register work through the UoW so CPQ's transaction management controls commit order.
  4. Enter the class name in CPQ Settings > Order Management > Order Plugin.

Why not a trigger: Order triggers may fire multiple times during CPQ's order creation sequence. The OrderPlugin gives deterministic pre/post hooks with access to CPQ's internal Unit of Work.

Pattern 3: ConfigurationInitializerPlugin to Pre-Select Options

When to use: The configuration screen must open with specific product options already selected or specific attribute values pre-populated based on the opportunity, account segment, or another quote field.

How it works:

  1. Create an Apex class implementing SBQQ.ConfigurationInitializerPlugin.
  2. Override initialize(SBQQ.ProductModel productModel, String configurationId).
  3. Mutate productModel to set default option quantities or attribute values.
  4. Register the class in CPQ Settings under the Configuration section.

Decision Guidance

SituationRecommended ApproachReason
New calculator customization, org on modern CPQ packageJS QCP (SBQQ__CustomScript__c)Modern supported approach; seven lifecycle hooks; no Apex compilation required
Existing Apex QuoteCalculatorPlugin, need to extendExtend existing Apex plugin OR migrate to JS QCPCannot mix; extending keeps one active type; migration preferred long-term
Logic needed during order creationApex OrderPluginNo JS equivalent exists for OrderPlugin
Logic needed during contract creationApex ContractingPluginNo JS equivalent exists for ContractingPlugin
Custom product catalog filteringApex ProductSearchPluginExposes search() returning filtered SObject lists
Pre-populate configurator optionsApex ConfigurationInitializerPluginFires before the configuration screen loads
Pricing logic expressible as price rulesDeclarative price rules (no plugin)Plugins add deployment and maintenance cost; use declarative where sufficient
Async callout needed inside calculatorApex QuoteCalculatorPlugin + SBQQ.CalculateCallbackOnly Apex plugin + callback supports async patterns; JS QCP can use JS Promises with fetch

  1. Identify which plugin type is required. Match the business requirement against the plugin taxonomy table. Confirm with the requester whether the hook fires in the right lifecycle phase (e.g., before vs. after price rules).
  2. Check for conflicting active plugins. Query SBQQ__CustomScript__c for active JS QCP records and inspect CPQ Settings registration fields. Identify any existing Apex plugin of the same type before writing a new one.
  3. Implement the plugin class or JS QCP code. Follow the exact method signatures from the CPQ Plugins Developer Guide. For Apex, ensure the class is global and with sharing unless the plugin needs system-mode access for a documented reason. For JS QCP, ensure every hook returns a Promise.
  4. Register the plugin in CPQ Settings. Enter the Apex class name in the correct Settings field, or activate the SBQQ__CustomScript__c record. Verify no other plugin of the same type is registered.
  5. Test in a sandbox with full CPQ calculation flows. Add a line, change quantity, apply discounts, and save the quote. Verify the plugin fires at the expected lifecycle point using debug logs with SBQQ category at FINEST level.
  6. Validate governor limit headroom. Review debug logs for CPU time and SOQL counts inside the plugin execution. CPQ's own calculation logic consumes significant limits; plugin code must be lean.
  7. Deploy and verify post-deployment. Confirm the CPQ Settings record retains the plugin registration after deployment (Settings records are not metadata — they live in the org's data layer and must be set manually or via a post-install script).

Review Checklist


Salesforce-Specific Gotchas

  1. CPQ Settings is data, not metadata — Plugin registration fields on SBQQ__CustomActionSettings__c are stored as org data, not in a .settings metadata file. A change set or package deployment does not carry these values. After every deployment, you must manually update or script the Settings record to re-register the plugin class name.
  2. JS QCP hooks that return undefined hang the calculator indefinitely — CPQ's calculation engine awaits the Promise returned by each hook. If a hook function has no explicit return statement, it returns undefined rather than a resolved Promise. The quote save UI spins without error until the user's session times out. Always end every hook with return Promise.resolve() even when no changes are made.
  3. Apex triggers on SBQQ__QuoteLine__c overwrite values set during calculation — CPQ performs multiple recalculation passes on save. An after update trigger that sets a field on SBQQ__QuoteLine__c runs between passes; CPQ's next pass reads the stored line and overwrites the field with its calculated value. The trigger's changes appear to stick briefly then revert. Use JS QCP or Apex plugin hooks — not triggers — for any field that CPQ owns.
  4. Only the first registered plugin of each type is called — CPQ does not chain multiple plugins of the same type. If SBQQ__OrderPlugin__c is already set to ExistingOrderPlugin, registering a second class NewOrderPlugin in the same field simply replaces the first. There is no multi-plugin chaining mechanism; you must combine logic into a single class or use a dispatcher pattern.
  5. global access modifier is mandatory on Apex plugin classes — CPQ plugin interfaces are defined inside the managed SBQQ namespace. Implementing a managed-package interface requires the implementing class to be global. An public implementing class compiles successfully in a scratch org or sandbox but throws a runtime System.TypeException when CPQ tries to instantiate it through the interface.

Output Artifacts

ArtifactDescription
Apex plugin classglobal class MyPlugin implements SBQQ.<PluginInterface> with all required methods implemented
JS QCP scriptJavaScript object with named hook functions stored in SBQQ__CustomScript__c.SBQQ__Code__c
CPQ Settings registration stepsWhich field to update and what value to enter for the specific plugin type
Plugin test planOrdered list of CPQ UI actions that confirm the plugin fires and produces correct output

Package files