Constraint Modeling
Language (CML)
The recommended approach for product configuration rules in Agentforce Revenue Management. Understand CML's core concepts, constraint types, and how to apply them to Comuniqa's bundle and qualification scenarios.
Foundational Definitions
CML is a domain-specific language used to define constraint models for complex product configuration. It describes real-world entities, their relationships, and business rules declaratively โ without extensive custom code. The constraint engine compiles CML into a working configuration model at runtime.
Constraint Model
The complete CML file describing a product's structure, attributes, and rules. Compiled by the constraint engine when a product is configured in Product Configurator.
Type
The foundational building block of CML. Represents an entity โ a product, bundle, component, or class. Similar to a class in object-oriented programming.
Variable
A property or characteristic defined within a type. Can be a string, number, boolean, date, or picklist. Represents product attributes and fields.
Relationship
Defines how types are associated. In Revenue Cloud, relationships represent the product structure in a bundle โ the root product's relationship to its components.
Constraint
A logical rule applied to types, variables, or relationships. Enforces business logic โ e.g. "if Contract Term is 24 months, Watch Device Type must be specified".
External Variable
A global CML variable set by the environment that launches the constraint engine. Used to import sales transaction fields from the Context Definition.
Group Type
A special CML type representing a PCM product component group. Identified by minInstanceQty and maxInstanceQty annotations. Controls how many components a customer can select from a group.
Rule
A specific type of constraint: require, exclude, hide, disable, message, preference, recommend. Each triggers a different runtime behavior in Product Configurator.
How CML Fits Into the Revenue Cloud Architecture
Bundle Structure
Import + Write Rules
Compiles CML
Runtime Enforcement
Valid Configuration
CML vs Business Rules Engine โ Why CML Wins
Salesforce Revenue Cloud offers two approaches to writing product configuration and qualification rules: the Business Rules Engine (BRE) and Constraint Modeling Language (CML). For all advanced implementations, CML is the recommended path.
| Dimension | BRE (Legacy) | CML (Recommended) |
|---|---|---|
| Approach | Point-and-click Decision Tables + Expression Sets | Declarative domain-specific language with full IDE support |
| Complexity ceiling | Limited โ struggles with nested bundle constraints and multi-attribute rules | Handles highly complex constraint models with type hierarchies and group nesting |
| Bundle group support | Basic cardinality only | Full Group Type support with minInstanceQty / maxInstanceQty and dot-notation constraints |
| Readability | Rules scattered across multiple records; hard to audit | All rules in one CML file โ readable, versionable, reviewable |
| Performance tools | Limited debugging | Full Apex debug log with execution stats, backtrack counts, and constraint violation tracing |
| Product Selling Model | Not supported in rules | Can set PSM via constraint: productSellingModel == PSM_ID |
| Recommendations | Not available | recommend rule for AI-assisted product suggestions |
| Salesforce guidance | Continue supporting pre-Winter '26 models | Recommended for all new implementations (Winter '26+) |
CML Core Concepts
1 โ Types
Types are the building blocks of CML. Every product, bundle, and component is represented as a type. Types support inheritance โ a subtype inherits all variables and constraints from its parent type.
// Basic type declaration type ComuniqaGo; // Type hierarchy โ subtype inherits from parent type MobileProduct; type ComuniqaGo : MobileProduct; // inherits all MobileProduct variables type ComuniqaGoYouth : ComuniqaGo; // inherits ComuniqaGo + MobileProduct
2 โ Variables
Variables define the properties of a type. They can have fixed domains (allowed values), be calculated from other variables, or be mapped from context definitions. Annotations control runtime behaviour.
type ComuniqaGo : LineItem { // Picklist variable with fixed domain @(defaultValue = "No Contract", sequence=1) string Contract_Term = ["No Contract", "12 Months", "24 Months"]; // Picklist variable โ SIM format @(defaultValue = "Physical SIM", sequence=2) string SIM_Format = ["Physical SIM", "eSIM"]; // Read-only โ populated by fulfillment system @(configurable = false) string ICCID; // Static descriptor โ no domain, value fixed in catalog string Voice_Calls_Included = "Unlimited"; }
3 โ External Variables & Context Path
External variables pull values from the sales transaction context โ account data, customer fields, or header values. They are the bridge between CML and the customer's real data.
// External variables set by the ProductDiscoveryContext @(contextPath = "SalesTransaction.customerAge") extern int customerAge = 0; @(contextPath = "SalesTransaction.outstandingAmount") extern decimal(2) outstandingAmount = 0.00; @(contextPath = "SalesTransaction.creditScore") extern int creditScore = 999;
4 โ Relationships & Group Types
Relationships define how bundle components connect to their parent. In Winter '26+, product component groups from PCM are imported as Group Types โ a distinct CML type with minInstanceQty and maxInstanceQty annotations that mirror PCM cardinality.
// Partner VAS Group โ Max 1 enforces mutual exclusivity @(minInstanceQty=0, maxInstanceQty=1) type PartnerVASGroup { relation moviMundoPlus : MoviMundoPlus; relation moviMundoBasic : MoviMundoBasic; } // Value-Added Services Group โ Max 3 @(minInstanceQty=0, maxInstanceQty=3) type ValueAddedServicesGroup { relation familyBundle : ComuniqaFamilyGoBundle; relation goTwo : ComuniqaGoTwo; relation goSharedTwo : ComuniqaGoSharedTwo; } // Bundle root uses group types as variables type ComuniqaGoBundleRoot : LineItem { PartnerVASGroup partnerVASGroup; ValueAddedServicesGroup vasGroup; // Constraints reference groups via dot notation }
partnerVASGroup.moviMundoPlus.someAttribute. The old syntax without group references is only supported for pre-Winter '26 constraint models.
Constraint Types โ Complete Reference
The core constraint type. Defines a statement that must always be true. Supports all logical operators: &&, ||, -> (implication), <-> (equivalence), ^ (xor), ?: (conditional).
type ComuniqaGoTwo : LineItem { string Watch_Device_Type = ["Apple Watch", "Samsung Galaxy Watch"]; string Watch_IMEI; // Implication: if device type is set, IMEI must be present constraint(Watch_Device_Type != null -> Watch_IMEI != null, "Watch IMEI is required when a device type is selected"); // Conditional operator: eSIM requires eSIM-capable device constraint(Watch_Device_Type == "Apple Watch" ? Watch_IMEI != null : Watch_IMEI != null); }
Key rule: The constraint engine never overrides user input. If a user sets a value that violates a constraint, the engine displays an error โ it does not silently change the user's value. The exception is the exclude() rule.
Automatically adds a required component to a relationship when specified conditions are met. Can also set attribute values on the required component.
type ComuniqaGoBundleRoot : LineItem { // If Go! Two is added, SIM must also be present require(vasGroup.goTwo[ComuniqaGoTwo] > 0, simGroup.sim[SIMCard]); // Require with attribute values set on the required component require(vasGroup.goTwo[ComuniqaGoTwo] > 0, simGroup.sim[SIMCard]{ SIM_Format = "eSIM" }, "Apple Watch requires eSIM โ SIM format has been set automatically"); }
Note: When assigning a require rule to a virtual bundle, set one Product Selling Model Option on the required product to Default.
Automatically removes a specific component from a relationship when a condition is met. Unlike other constraints, the engine overrides user input to enforce the exclusion.
type ComuniqaGoBundleRoot : LineItem { // Cannot have MoviMundo AND Vida Digital Pass at the same time // PCM cardinality handles this, but CML can also enforce explicitly exclude(subscriptionsGroup.vidaDigital[VidaDigitalPass] > 0, partnerVASGroup.moviMundoPlus[MoviMundoPlus], "Vida Digital Pass is mutually exclusive with MoviMundo Plus+"); exclude(subscriptionsGroup.vidaDigital[VidaDigitalPass] > 0, partnerVASGroup.moviMundoBasic[MoviMundoBasic], "Vida Digital Pass is mutually exclusive with MoviMundo Basic+"); }
Important: The exclude rule target must be a leaf type โ a type with no children. This is the only constraint that overrides user input automatically.
Controls the visibility and interactivity of components, attributes, and attribute values in the Product Configurator UI. Used to progressively show relevant options based on current configuration state.
type ComuniqaGoTwo : LineItem { string Watch_Device_Type = ["Apple Watch", "Samsung Galaxy Watch"]; string Watch_IMEI; // Hide IMEI field until device type is selected rule(Watch_Device_Type == null, "hide", "attribute", "Watch_IMEI"); } type ComuniqaGoBundleRoot : LineItem { // Disable Shared Two if standard Go! Two already selected rule(vasGroup.goTwo[ComuniqaGoTwo] > 0, "disable", "relation", "vasGroup", "type", "ComuniqaGoSharedTwo"); // Hide Phone Number Category unless type = Premium rule(phoneNumGroup.phoneNumber.Phone_Number_Type != "Premium", "hide", "attribute", "Phone_Number_Category"); }
Displays a message to users based on specified conditions. Supports three severity levels: Info (gray, non-blocking), Warning (yellow, blocks next step), Error (red, blocks current task).
type ComuniqaGoBundleRoot : LineItem { // Info: remind agent about eSIM process message(simGroup.sim.SIM_Format == "eSIM", "eSIM activation requires customer to scan QR code within 24 hours", "Info"); // Warning: no-contract customers cannot select 24-month device message(basePlan.comuniqaGo.Contract_Term == "No Contract" && vasGroup.goTwo[ComuniqaGoTwo] > 0, "No-contract customers pay full device price for Go! Two", "Warning"); // Error: block if GoogleEmail missing on YouTube Mundo message(vasGroup.goTwo[ComuniqaGoTwo] > 0 && vasGroup.goTwo.GoogleEmail == null, "Google Account email is required to activate YouTube Mundo", "Error"); }
Defines valid combinations of attribute values in rows. Each row inside {} is one valid combination. Can also import data from a Salesforce object using the SalesforceTable keyword.
type MoviMundoPlus : LineItem { int Simultaneous_Devices; string Video_Quality; string Plan_Tier; // Valid combinations of devices, quality, and tier constraint( table(Simultaneous_Devices, Video_Quality, Plan_Tier, {1, "HD", "Basic"}, {2, "FHD", "Plus"}, {4, "4K", "Premium"} ) ); // OR: import from Salesforce object // constraint(table(Display_Size,Memory, // SalesforceTable("MoviMundoPlan__c","Devices__c,Quality__c"))) }
Adds product recommendations that appear at runtime via an invocable action in a flow. Unique to CML โ not available in BRE.
type ComuniqaGo : LineItem { // If agent selects 24-month contract, recommend Go! Two rule(Contract_Term == "24 Months", "recommend", "relation", "vasGroup"); // If eSIM selected, recommend MoviMundo Plus+ (streaming) rule(SIM_Format == "eSIM", "recommend", "type", "MoviMundoPlus"); }
Sets the Product Selling Model for a type based on a constraint. Updates PSM for new line items only โ cannot update PSM on existing quote lines.
type ComuniqaGo : LineItem { @(defaultValue = "No Contract") string Contract_Term = ["No Contract", "12 Months", "24 Months"]; // Automatically set the correct PSM based on contract selection constraint(Contract_Term == "12 Months" -> productSellingModel == "PSM_12M_ID"); constraint(Contract_Term == "24 Months" -> productSellingModel == "PSM_24M_ID"); constraint(Contract_Term == "No Contract" -> productSellingModel == "PSM_EVERGREEN_ID"); }
Replace PSM_12M_ID etc. with the actual Salesforce record IDs of your Product Selling Model records โ found in the URL when viewing the PSM record in your org.
Comuniqa CML Examples โ Real Implementation Scenarios
A customer can select only one of MoviMundo Plus+, MoviMundo Basic+, or Vida Digital Pass. PCM cardinality (Max=1 on Partner VAS group) handles the group-level enforcement. CML adds explicit error messaging and cross-group exclusion with the Subscriptions group.
type ComuniqaGoBundleRoot : LineItem { PartnerVASGroup partnerVASGroup; SubscriptionsGroup subscriptionsGroup; // Explicit cross-group mutual exclusivity via CML exclude(subscriptionsGroup.vidaDigitalPass[VidaDigitalPass] > 0, partnerVASGroup.moviMundoPlus[MoviMundoPlus], "Remove MoviMundo Plus+ โ Vida Digital Pass already covers partner content"); exclude(subscriptionsGroup.vidaDigitalPass[VidaDigitalPass] > 0, partnerVASGroup.moviMundoBasic[MoviMundoBasic], "Remove MoviMundo Basic+ โ Vida Digital Pass already covers partner content"); // Message when agent tries to add both message(partnerVASGroup.moviMundoPlus[MoviMundoPlus] > 0 && subscriptionsGroup.vidaDigitalPass[VidaDigitalPass] > 0, "Conflicting partner content selections detected โ only one allowed", "Error"); } @(minInstanceQty=0, maxInstanceQty=1) type PartnerVASGroup { relation moviMundoPlus : MoviMundoPlus; relation moviMundoBasic : MoviMundoBasic; } @(minInstanceQty=0, maxInstanceQty=1) type SubscriptionsGroup { relation vidaDigitalPass : VidaDigitalPass; }
When a customer adds Comuniqa Go! Two, the Watch Device Type is required. If Apple Watch is selected, an eSIM is recommended. The PSM is automatically set based on the contract term.
type ComuniqaGoTwo : LineItem { @(defaultValue = "Apple Watch", sequence=1) string Watch_Device_Type = ["Apple Watch", "Samsung Galaxy Watch"]; @(configurable = false) string Watch_IMEI; // fulfillment-populated, not agent-editable string Paired_Mobile_Number; // IMEI visible only after activation โ hide in ordering rule(true, "hide", "attribute", "Watch_IMEI"); // Paired number required when any device type is selected constraint(Watch_Device_Type != null -> Paired_Mobile_Number != null, "Paired mobile number is required for watch service activation"); } type ComuniqaGoBundleRoot : LineItem { ValueAddedServicesGroup vasGroup; // If Apple Watch selected, recommend eSIM on SIM card rule(vasGroup.goTwo.Watch_Device_Type == "Apple Watch", "recommend", "relation", "simGroup"); // Message: Samsung watch users must use Physical SIM message(vasGroup.goTwo.Watch_Device_Type == "Samsung Galaxy Watch" && simGroup.sim.SIM_Format == "eSIM", "Samsung Galaxy Watch requires Physical SIM โ please update SIM format", "Warning"); }
Comuniqa Go! Youth is restricted to customers aged 18 or under. The credit score check gates term-based subscriptions. Both rules use external variables mapped from the ProductDiscoveryContext.
// External variables from ProductDiscoveryContext @(contextPath = "SalesTransaction.customerAge") extern int customerAge = 99; @(contextPath = "SalesTransaction.creditScore") extern int creditScore = 999; @(contextPath = "SalesTransaction.outstandingAmount") extern decimal(2) outstandingAmount = 0.00; type ComuniqaGoYouth : LineItem { // Age gate: block if customer is over 18 constraint(customerAge <= 18, "Comuniqa Go! Youth is only available for customers aged 18 or under"); // Block if outstanding debt exists constraint(outstandingAmount == 0, "New subscriptions are not available while account has an outstanding balance"); } type ComuniqaGo : LineItem { @(defaultValue = "No Contract") string Contract_Term = ["No Contract", "12 Months", "24 Months"]; // Credit check: term contracts require credit score >= 600 constraint( (Contract_Term == "12 Months" || Contract_Term == "24 Months") -> creditScore >= 600, "Credit score of 600 or above is required for contract subscriptions"); // Outstanding balance blocks all new plan subscriptions constraint(outstandingAmount == 0, "Outstanding balance must be cleared before adding a new plan"); }
Capabilities & Limitations
What CML Can Do
| Capability | Type | Notes |
|---|---|---|
| Logical constraints (if/then/else) | Core | Full boolean logic with implication, equivalence, conditional |
| Group type cardinality enforcement | Core | minInstanceQty / maxInstanceQty on group types |
| Cross-group mutual exclusivity | Core | exclude() across group types in the same bundle |
| Hide/Disable attributes & components | Core | Progressive UI disclosure based on configuration state |
| Auto-add components (require) | Core | With optional attribute value pre-population |
| Auto-remove components (exclude) | Core | Only constraint that overrides user input |
| Messaging (Info / Warning / Error) | Core | Error blocks current task; Warning blocks next step |
| Table constraints (valid combinations) | Core | Can import rows from Salesforce objects via SalesforceTable |
| Product Selling Model in constraint | Advanced | Sets PSM on new line items only |
| Product recommendations | Advanced | Via recommend rule + invocable action in flow |
| External variables from context | Advanced | Maps SalesTransaction fields into CML for qualification rules |
| Type hierarchies (inheritance) | Advanced | Subtypes inherit all variables and constraints |
| Variable functions (sum, min, max, count) | Advanced | Aggregate across descendants of a type |
Documented Limitations
| Limitation | Impact |
|---|---|
split=true not supported for child products in a dynamic bundle | Use split=false or omit split annotation for dynamic bundle children |
| Cannot write a constraint directly on a group's attribute to apply to all components | constraint(accessoryGroup.Wireless == true) is invalid โ must reference specific component types via dot notation |
| productSellingModel constraint cannot update PSM on existing quote lines | PSM is set only when the line item is first created โ amendments do not re-trigger PSM constraints |
| Constraint engine never overrides user input (except exclude rule) | If a user sets a value that violates a constraint, an error is shown โ not silently corrected |
| The entire bundle must be in the CML model to constrain a child product | Cannot write a standalone CML model for just one child product โ the bundle root must be included |
| Only lowest-level groups imported into CML for nested group structures | Cardinality of only leaf-level groups is evaluated at runtime by the constraint engine |
CML Best Practices
From the official CML User Guide โ these practices prevent performance degradation and unexpected behavior.
The constraint engine tests every quantity value up to the maximum. relation engine : Engine; (no cardinality) makes the engine test 1 through 9,999. relation engine : Engine[0..1]; limits this to 0 or 1 โ dramatically faster.
Comuniqa application: All Comuniqa Go! component relationships should specify explicit cardinality matching PCM settings โ e.g. relation sim : SIMCard[1..1];, relation goTwo : ComuniqaGoTwo[0..1];
A decimal(2) variable with domain [0..3] tests 0.00, 0.01, 0.02... up to 2.99 โ 300 permutations. An int with the same domain tests only 0, 1, 2, 3 โ 4 permutations. Use int for Member Count, Simultaneous Devices, and other whole-number attributes on Comuniqa products.
Each additional value in a domain multiplies the engine's search space. Comuniqa's Contract Term picklist (3 values), SIM Format (2 values), and Watch Device Type (2 values) are already well-scoped. Avoid open-ended text domains on configurable attributes โ use picklists where possible.
Inline expressions (area = length * width) force the engine to evaluate the expression at every choice point. A constraint (constraint(area == length * width)) is evaluated only when needed. For Comuniqa, bundle total price calculations and member count validations should use constraints, not inline variable assignments.
Multiple separate relationships on a type multiply constraint evaluations. Where possible, combine components of the same logical category into one relationship. Comuniqa's group type structure (PartnerVASGroup, ValueAddedServicesGroup) already follows this pattern correctly.
When constraints depend on attributes being set in a specific order, use @(sequence=N) to tell the engine the evaluation order. For Comuniqa Go!, Contract Term (sequence=1) should be evaluated before SIM Format (sequence=2) because downstream constraints depend on the contract term selection.
When you need to both auto-add a product AND set its attributes, use two separate constraints instead of one combined constraint. This improves clarity and reduces constraint engine backtracking.
// โ CORRECT โ separate constraints constraint(vasGroup.goTwo[ComuniqaGoTwo] > 0, simGroup.sim[SIMCard] > 0); constraint(simGroup.sim[SIMCard] > 0, simGroup.sim.SIM_Format == "eSIM"); // โ AVOID โ combined in one constraint constraint(vasGroup.goTwo[ComuniqaGoTwo] > 0, simGroup.sim[SIMCard] > 0 && simGroup.sim.SIM_Format == "eSIM");
Architectural Decision Guidance
When to Use CML vs Other Mechanisms
| Scenario | Use | Why |
|---|---|---|
| Mutual exclusivity between bundle components (e.g. MoviMundo vs Vida Digital) | CML exclude() | Cross-group exclusion requires CML โ PCM cardinality only handles within-group |
| Hide/show attributes based on other selections | CML rule() hide | Progressive disclosure must happen at configuration runtime โ only CML can do this |
| Customer eligibility checks (age, credit, debt) | CML + Context | External variables in CML map cleanly from ProductDiscoveryContext โ cleaner than BRE Expression Sets |
| Auto-set Product Selling Model on contract selection | CML productSellingModel | Only CML constraint can set PSM based on attribute value โ not possible in BRE or PCM alone |
| Valid attribute combinations (e.g. device tier + streaming quality) | CML table constraint | Table constraint is the most maintainable pattern โ rows can be Salesforce-object-driven |
| Group cardinality (min/max components in a group) | PCM Group Cardinality | PCM is the source of truth for bundle structure โ CML reads from it, not replaces it |
| Pricing discounts based on attribute values | Pricing Procedure | CML does not calculate prices โ Pricing Engine reads attributes and applies discounts |
| Fulfillment rules (e.g. provision ICCID) | DRO | CML operates at configuration time โ DRO handles post-order fulfillment logic |
Knowledge Check โ 10 Questions
All questions grounded in the Comuniqa product model and the official CML User Guide (Edition 2.3, Winter '26).