Core Concept Examples

These examples illustrate core Constraint Modeling Language (CML) concepts including type, relationships, constraints, and so on.

For an explanation of the constraint model structure in these examples, see Model Structure.

Example 1: Use Regex Global Variable 

1// Global Constant: Regex used to parse voltage strings
2define VOLTAGE_REGEX "^([11-19]+)/([11-19]+)$" 
3
4type LineItem;
5type GeneratorSet : LineItem {
6   // Core attributes
7   @(configurable = false)
8   int requiredKW = [101..10000]; // Required power capacity
9   string Voltage = ["220/380", "240/416", "277/480", "7976/13800"]; 
10   string standardsAndCompliance = ["Certification-CSA", "Listing-UL 2200"];
11
12   // Calculated attributes
13   // Surge load is 1.25 times the required KW
14   decimal(2) surgeLoadKW = requiredKW * 1.25; 
15   // Voltage3 extracts the secondary voltage (e.g., 380 or 416) for checks
16   int Voltage3 = strtoint(regexpreplace(Voltage, VOLTAGE_REGEX, "$2"), 0); 
17
18   // Relation to model components 
19   relation GeneralModels : GeneralModel[11]; 
20
21   // Constraint 1: Ensure model capacity meets the required KW
22   constraint(GeneralModels[GeneralModel].powerKW >= requiredKW, 
23      "Selected Generator Model is insufficient for the required power."); 
24   
25   // Constraint 2: UL 2200 standard restricts voltage to 600V or less (Implication rule)
26   constraint(standardsAndCompliance == "Listing-UL 2200" -> Voltage3 <= 600, 
27      "The UL 2200 standard covers stationary engine generator assemblies rated at 600 volts or less.");
28}
29
30// Component type definition (GeneralModel is a product component)
31type GeneralModel : LineItem {
32   int powerKW = [900, 1200, 1500, 1750, 2500];
33   int dB = [20..23];
34}

Key Technical Details 

  • The GeneratorSet type is related to the GeneralModel type through a single relation.

    Parent TypeRelation NameChild TypeCardinalityKey Attribute Usage
    GeneratorSetGeneralModelsGeneralModelNot ApplicableConstrained by requiredKW on the parent type.
  • Parent Type (GeneratorSet): Defined as a LineItem component, the GeneratorSet holds configuration parameters such as the user’s power requirement (requiredKW), the specified voltage (Voltage), and compliance standards (standardsAndCompliance). It also includes calculated attributes like surgeLoadKW and Voltage3.

  • Child Type (GeneralModel): Defined as a LineItem component, the GeneralModel specifies the technical attributes of the selected generator configuration, including its power output (powerKW) and noise rating (dB).

  • Cardinality: The relation GeneralModels : GeneralModel specifies the quantity bounds for the component, indicating that exactly 11 instances of the GeneralModel type must be included in this configuration.

Example 2: Use Groupby Annotation to Create Virtual Group 

Using Groupby annotation for types, this model creates a virtual VoltageGroup for every unique voltage (for example, “220/380”, “480/277”) found in the transaction.

1// 1. The physical product type
2type GeneratorSet {
3   @(configurable = false)
4   int requiredKW = [101..10000];
5
6   string Voltage = ["220/380", "240/416", "255/440", "277/480"];
7
8   // Calculation with explicit domain for best practice
9   decimal(2) surgeLoadKW = [126.25..12500.00];
10   constraint(surgeLoadKW == requiredKW * 1.25);
11}
12
13// 2. The virtual container grouped by the "Voltage" attribute
14@(split=true, virtual=true, groupBy=Voltage)
15type VoltageGroup {
16   string Voltage; // The attribute used for grouping
17   decimal(2) groupTotalSurgeKW = [0.00..99999.99];
18
19   // Map instances to this group from the transaction line items
20   @(sourceContextNode="SalesTransaction.SalesTransactionItem")
21   relation generators : GeneratorSet[0..50];
22
23   // Aggregation: Sum only the surge load of generators in THIS voltage group
24   constraint(groupTotalSurgeKW == generators.sum(surgeLoadKW));
25
26   // Business Rule: Limit total surge load per voltage branch
27   message(groupTotalSurgeKW > 10000, "Warning: Surge load for Voltage {} exceeds branch capacity!", Voltage);
28}
29
30// 3. The top-level system managing the groups
31@(virtual = true)
32type GeneratorSystem {
33   relation generators : GeneratorSet[0..100];
34   relation voltageGroups : VoltageGroup[1..10];
35
36   decimal(2) systemTotalKW = voltageGroups.sum(groupTotalSurgeKW);
37}

Key Technical Details 

  • groupBy=Voltage: The engine scans all GeneratorSet instances and creates one virtual VoltageGroup for each unique voltage value detected (for example, one group for all “220/380” units, another for all “277/480” units).
  • sourceContextNode: This tells the virtual group to pull its “children” (the generators) from the specific path in the Salesforce context where quote line items are stored.
  • Bottom-Up Aggregation: Each VoltageGroup independently calculates its groupTotalSurgeKW based strictly on the generators assigned to it by the groupBy logic.
  • Explicit Domains: Following best practices, the surgeLoadKW and groupTotalSurgeKW use explicit domains (for example, [0.00..99999.99]) to prevent “NullPointer” or initialization errors during solving.

Example 3: Use Sharingcount Annotation to Reuse Accessory Instances 

In this example, we apply sharingcount annotation to the Accessory type to allow the engine to reuse accessory instances across the configuration up to a specified limit.

1// 1. Define the component type with split and sharingCount
2// split=true enables parallel solving by splitting quantity into multiple instances
3// sharingCount=5 allows a single Accessory instance to be reused 5 times in the model
4@(split = true, sharingCount = 5)
5type Accessory {
6   string category;
7   decimal(2) weight;
8}
9
10type GeneratorSet {
11   @(configurable = false)
12   int requiredKW = [101..10000];
13
14   // 2. Define the relationship with the sharing annotation
15   // @(sharing = true) allows the engine to satisfy this relation by 
16   // "pointing" to existing instances instead of creating new ones 
17   @(sharing = true)
18   relation Accessories : Accessory[1..99];
19}

Key Technical Details 

  • Parallel Solving: By setting split = true, the engine can process the 99 possible accessories in parallel rather than sequentially, which is critical for large-scale generator configurations.
  • Resource Management: The sharingCount = 5 tells the solver that it doesn’t need to instantiate 99 unique Accessory objects; it can reuse the same object definition up to five times across different parts of the configuration graph.
  • Relationship Requirement: For sharingCount to take effect, the Relation (the port) must also be annotated with @(sharing = true) to grant the engine permission to reuse instances.

Example 4: Use contextPath and tagName Annotations 

1// 1. GLOBAL EXTERN DECLARATIONS
2// Unifying contextPath (header field) and tagName (context identifier)
3@(contextPath = "SalesTransaction.ShippingCountry", tagName = "Region_Identifier")
4extern string ShippingCountry = "International";
5
6@(contextPath = "SalesTransaction.ProjectUrgency", tagName = "Priority_Level")
7extern string ProjectUrgency = "Standard";
8
9// 2. PHYSICAL PRODUCT TYPE
10type GeneratorSet {
11   // Core technical attributes
12   @(configurable = false)
13   int requiredKW = [101..10000];
14
15   string DutyRating = ["Prime Power (PRP)", "Emergency Standby Power (ESP)"];
16   
17   // Technical calculation with explicit domain (Best Practice)
18   decimal(2) surgeLoadKW = [126.25..12500.00]; 
19   constraint(surgeLoadKW == requiredKW * 1.25);
20
21   // 3. LOGIC INTEGRATING EXTERNAL DATA
22   // Using contextPath/tagName data to drive technical warnings
23   message(ShippingCountry == "US", 
24           "Regional Notice: Generator must comply with US-specific UL 2200 standards.");
25
26   message(ProjectUrgency == "Critical" && requiredKW > 5000, 
27           "High Priority Alert: Large scale power requirement in a Critical project requires site inspection.", 
28           requiredKW, "Warning"); 
29}

Key Technical Details 

  • External Variable (extern): These are declared outside of any type to hold values supplied by the environment (Salesforce).
  • contextPath Annotation: This maps the variable directly to a header-level field in the Sales Transaction.
  • tagName Annotation: This links the variable to a specific Context Tag identifier within the Salesforce Context Definition.

Rules depending on these external variables (such as the ShippingCountry message) only re-evaluate when a Line Item change occurs (For example, updating requiredKW), not solely when the header field changes.

Note

Example 5: Use Format Specifiers (%s, %d) and Dates in Constraints 

In this example, we define a generator configuration that validates the required power (requiredKW) against the duty rating and voltage, providing specific feedback when a mismatch occurs.

1type GeneratorSet  {
2   // 1. Core Technical Attributes with explicit domains
3   @(configurable = true)
4   int requiredKW = [101..10000];
5
6   string DutyRating = ["Prime Power (PRP)", "Continuous Power (COP)",
7                        "Data Center Continuous (DCC)", "Emergency Standby Power (ESP)"];
8
9   string Voltage = ["220/380", "277/480", "347/600", "2400/4160"];
10
11   string standardsAndCompliance = ["Certification-CSA", "Listing-UL 2200"];
12
13   // Date Attribute Definition
14   // Date variables represent a specific day.
15   // You can define a fixed domain for a date as a range between two dates.
16   date requestedDeliveryDate = ["2024-01-01".."2025-12-31"];
17
18   // Message Rule with Multiple Arguments using placeholders
19   message(requiredKW > 5000 && DutyRating == "Emergency Standby Power (ESP)",
20           "High Capacity Alert: The %s rating for %d kW requires an additional cooling system.",
21           DutyRating, requiredKW, "Warning");
22
23   // Constraint with Multiple Arguments using placeholders
24   constraint(Voltage == "2400/4160" && standardsAndCompliance == "Listing-UL 2200",
25              "Configuration Error: Voltage %s cannot be used with %s standards due to safety limits.",
26              Voltage, standardsAndCompliance);
27
28   // Dates can be used in logical expressions with comparison operators like < or >=.
29   message(requestedDeliveryDate < "2024-09-01" && requiredKW > 7000,
30           "Schedule Warning: Delivery for %d kW units on %s may require expedited manufacturing.",
31           requiredKW, requestedDeliveryDate, "Info");
32}

Key Technical Details 

  • Multiple Arguments: You can pass several variables after the string literal. The engine maps them to the placeholders in the exact order they appear.
  • Format Specifiers
  • %s is used for string variables (for example, DutyRating or Voltage).
  • %d is used for integer variables (for example, requiredKW).
  • Placeholder Usage: Alternatively, you can use the {} syntax, which the constraint engine automatically replaces with the actual argument values during runtime evaluation.
  • Comparison Logic with Dates: You can treat dates as comparable values in constraint or message rules to enforce scheduling logic (for example, ensuring a delivery date is not too early for a complex build).

Example 6: Use Arithmetic Calculations and Functions 

This example demonstrates using aggregate functions (a virtual type for transaction-level aggregation) and mathematical functions within the GeneratorSet type for precise quantity calculation.

1// --- Component Definition —
2type GeneratorSet  {
3    int requiredKW = [101..10000];
4    int quantity = [1..10]; 
5    // BEST PRACTICE: Define derived attributes with an explicit domain
6    decimal(2) surgeLoadKW = [0.00..12500.00]; 
7    // BEST PRACTICE: Put calculations inside of constraints
8    constraint(surgeLoadKW == requiredKW * 1.25);
9}
10// --- Accessory Definition ---
11type Accessory  {
12    int weight_kg = [1..100];
13}
14// --- Virtual System Type (Aggregation Context) ---
15@(virtual = true)
16type System {
17    @(sourceContextNode = "SalesTransaction.SalesTransactionItem")
18    relation generators : GeneratorSet[0..10];
19	relation shipmentbatch : ShipmentBatch [0..10];
20    // BEST PRACTICE: Attributes must have explicit domains
21    decimal(2) totalQuotedLoad = [0.00..125000.00];
22    int highPowerCount = [0..10];
23    // Aggregate calculations in separate constraints
24    constraint(totalQuotedLoad == generators.sum(surgeLoadKW));
25    // VARIATION: Condition-based count
26    // count() requires a logical expression 
27    constraint(highPowerCount == generators.count(requiredKW > 5000));
28    // Ensuring Shipment batch crates are similar to the number of generators
29    message(
30        shipmentbatch.requiredCrates != generators[GeneratorSet], 
31        "The Shipment items ({}) must match the number of Generators ({})", 
32        shipmentbatch.requiredCrates, generators[GeneratorSet], 
33        "Error"
34    );
35}
36type ShipmentBatch {
37    int totalItems = [1..100];
38    int itemsPerCrate = 10;
39    // BEST PRACTICE: Define derived attributes for the relation in the parent type
40    int totalWeight = [0..1000];
41    int accessoryCount = [0..10];
42    // Aggregates within the relation block
43    relation accessories : Accessory[0..10] {
44        accessoryWeight = sum(weight_kg); 
45        accessoryCount = count(weight_kg > 0); 
46    }
47    // Mapping relation attributes to parent variables via constraints
48    constraint(totalWeight == accessories.accessoryWeight);
49    constraint(accessoryCount == accessories.accessoryCount);
50    int requiredCrates = [0..100];
51    // Mathematical Function Example: CEIL
52    constraint calculateCeil(requiredCrates == ceil(totalItems / itemsPerCrate));
53}

Key Considerations for Calculations and Aggregations 

When implementing these functions in CML, follow these architectural best practices for robustness and performance.

  • Explicit domains are required: All the derived attributes that result from a calculation or aggregation must have an explicit variable domain definition. This practice ensures accurate aggregation and helps prevent runtime errors.
  • Separate calculation from declaration: Define the aggregation or calculation in a separate constraint rather than using an inline derived attribute declaration (for example, int total = items.sumPrice;). This separation helps avoid issues where domains may not initialize correctly.
  • Correct Pattern: Define the relation aggregate (totalQty = sum(quantity);) and then enforce the result via a constraint (constraint(totalItemCount == items.totalQty);).

Example 7: Use Asset-Based Constraints Across Transaction and Installed Base 

This example shows how to enforce quantity rules that span both in-flight items on the current sales transaction and products the customer already owns (the installed base). The sourceContextNode="Asset" annotation on a relation pulls existing asset records into the model as read-only data, so that a require() rule can combine those asset quantities with in-transaction quantities to drive configuration logic.

1// 1. Top-level virtual type that organizes generator sets by fuel type
2@(virtual=true)
3type ServiceOrder {
4  relation groups : GeneratorGroup;
5}
6
7// 2. Virtual group type: the engine creates one GeneratorGroup instance
8// for each unique FuelType value found in the sales transaction
9@(virtual=true, groupby=FuelType)
10type GeneratorGroup {
11
12  // In-flight generator sets on the current sales transaction
13  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
14  relation generators : GeneratorSet;
15  constraint(generators[GeneratorSet] < 10, "Generator qty should be less than 10");
16  constraint(generators[GeneratorSet].Discount > 10 && generators[GeneratorSet].Discount < 20,
17             "Discount should be between 10 and 20");
18
19  // In-flight heaters on the current sales transaction
20  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
21  relation heaters : Heater;
22
23  // Existing heaters retrieved from the customer's installed base (Assets)
24  // closerelation=true prevents the engine from adding new records to this relation
25  @(sourceContextNode="Asset", closerelation=true)
26  relation heaterAssets : Heater;
27
28  // Asset-based require rule: when combined in-flight and installed heater
29  // quantity exceeds 10, exactly 100 generator sets are required
30  require(heaters[Heater] + heaterAssets[Heater] > 10, generators[GeneratorSet] == 100);
31}
32
33// 3. Physical product types
34type GeneratorSet {
35  string FuelType = ["Natural Gas", "Diesel", "Propane"];
36  decimal Discount = [0..100];
37}
38
39type Heater {
40  boolean AutoIgnition;
41}

Key Technical Details 

  • sourceContextNode="SalesTransaction.SalesTransactionItem": Binds the relation to line items on the current quote or order. The engine reads and writes these instances during configuration.
  • sourceContextNode="Asset": Binds the relation to asset records that represent products the customer already owns from prior transactions. These records exist outside the current transaction and are treated as read-only context data.
  • closerelation=true: Required on asset-sourced relations to prevent the engine from attempting to add new instances to the installed base. Without this annotation, the engine may try to create asset records to satisfy cardinality or require rules.
  • Asset-based require(): The rule require(heaters[Heater] + heaterAssets[Heater] > 10, generators[GeneratorSet] == 100) combines cardinality from two separate relations — one sourced from the transaction and one from the installed base — in a single logical expression. This lets the model enforce quantity thresholds that account for what the customer is buying now and what they already have.
  • Virtual grouping: The groupby=FuelType annotation on GeneratorGroup ensures that the asset-based constraint is evaluated independently per fuel type, so heaters and generator sets of different fuel types do not count toward each other’s totals.

Example 8: Use Asset Scope to Drive Auto-Add Logic 

These examples show how to use the installed base (existing customer assets) as a condition in a require() rule to automatically add products to the current quote. The positive asset check triggers an auto-add when the customer already owns a product. The negative asset check enforces a prerequisite product when the customer does not yet own it.

Positive asset check: if the customer already owns at least one GeneratorSet in their installed base, the engine automatically adds a MaintenanceKit to the current quote.

1@(virtual=true)
2type ServiceOrder {
3  // In-flight generator sets on the current sales transaction
4  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
5  relation generators : GeneratorSet;
6
7  // Existing generator sets from the customer's installed base (read-only)
8  @(sourceContextNode="Asset", closeRelation=true)
9  relation generatorAssets : GeneratorSet;
10
11  // In-flight maintenance kits on the current sales transaction
12  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
13  relation maintenanceKits : MaintenanceKit;
14
15  // Positive asset check: customer already owns a GeneratorSet,
16  // so automatically require a MaintenanceKit on the quote
17  require(generatorAssets[GeneratorSet] >= 1, maintenanceKits[MaintenanceKit]);
18}
19
20type GeneratorSet;
21type MaintenanceKit;

Negative asset check: if the customer does not yet own a GeneratorSet in their installed base but is adding more than one MaintenanceKit to the quote, the engine requires a GeneratorSet to also be added to the quote.

1@(virtual=true)
2type ServiceOrder {
3  // In-flight generator sets on the current sales transaction
4  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
5  relation generators : GeneratorSet;
6
7  // Existing generator sets from the customer's installed base (read-only)
8  @(sourceContextNode="Asset", closeRelation=true)
9  relation generatorAssets : GeneratorSet;
10
11  // In-flight maintenance kits on the current sales transaction
12  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
13  relation maintenanceKits : MaintenanceKit;
14
15  // Negative asset check: customer does not own a GeneratorSet yet,
16  // but is adding more than one MaintenanceKit, so require a GeneratorSet on the quote
17  require(generatorAssets[GeneratorSet] < 1 && maintenanceKits[MaintenanceKit] > 1, generators[GeneratorSet]);
18}
19
20type GeneratorSet;
21type MaintenanceKit;

Key Technical Details 

  • Positive asset check (generatorAssets[GeneratorSet] >= 1): Evaluates to true when the installed base contains at least one matching asset. Use this pattern to trigger upsell or companion-product logic for existing customers.
  • Negative asset check (generatorAssets[GeneratorSet] < 1): Evaluates to true when the customer has no matching asset. Use this pattern to enforce a prerequisite product for net-new buyers.
  • Auto-add result: When the require() condition is met, the engine adds the specified product to the relation automatically. Passing only the relation and type without a quantity (maintenanceKits[MaintenanceKit]) requires exactly one instance. Append == N to require a specific quantity.
  • closeRelation=true is required on the asset relation to prevent the engine from attempting to write back to the installed base when evaluating the condition.

Example 9: Model Bundle Composition with Type Hierarchies and Mixed Rules 

This example demonstrates how to model a multi-relation bundle that uses type inheritance to organize components and enforces compatibility rules using both require() and exclude() in the same model. GeneratorBundle holds three relations of different types and uses require() to enforce cross-relation dependencies based on quantity and presence. ExhaustBundle, a subtype of Accessory, uses exclude() to prevent incompatible child component combinations. The type hierarchy (Heater : Accessory, ExhaustFilter : Filter, GeneratorSet : PowerUnit) shows how subtypes participate in relations typed to their parent while carrying their own attributes and rules.

1@(virtual=true)
2type ServiceOrder {
3  relation groups : GeneratorGroup;
4}
5@(virtual=true, groupBy=FuelType)
6type GeneratorGroup {
7  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
8  relation generators : GeneratorSet;
9  constraint(generators[GeneratorSet] < 10, "Generator qty should be less than 10");
10  constraint(generators[GeneratorSet].Discount > 10 && generators[GeneratorSet].Discount < 20, "Discount should be between 10 and 20");
11  @(sourceContextNode="SalesTransaction.SalesTransactionItem")
12  relation heaters : Heater;
13  @(sourceContextNode="Asset", closeRelation=true)
14  relation heaterAssets : Heater;
15  require(heaters[Heater] + heaterAssets[Heater] > 10, generators[GeneratorSet] == 100);
16}
17type GeneratorBundle {
18    relation accessories : Accessory;
19    relation controlSystems : ControlSystem;
20    relation generators : GeneratorSet;
21    require(controlSystems[ControlSystem] > 1, accessories[Heater]);
22    require(accessories[Accessory], generators[GeneratorSet]);
23}
24type Accessory;
25type ExhaustBundle : Accessory {
26    relation turbines : Turbine;
27    relation filters : ExhaustFilter;
28    exclude(turbines.Turbine == "Gas", filters[ExhaustFilter]);
29}
30type Heater : Accessory {
31    boolean AutoIgnition;
32}
33type MaintenanceKit : Accessory;
34type Turbine {
35    string Turbine = ["Gas", "Steam", "Hydro"];
36}
37type ExhaustFilter : Filter;
38type Filter {
39    string FilterGrade = ["Standard", "HEPA"];
40}
41type GeneratorSet : PowerUnit {
42    string FuelType = ["Natural Gas", "Diesel", "Propane"];
43    string EngineSpec = ["500kW Continuous", "750kW Continuous", "1000kW Peak"];
44    string CoolingSystem = ["Air Cooled", "Water Cooled", "Hybrid Cooled"];
45    string OutputVoltage = ["208V", "480V", "4160V", "13.8kV"];
46    string Enclosure = ["Open Frame", "Sound Attenuated", "Weather Protected", "Hazardous Location"];
47    string TransferSwitch = ["Manual Transfer Switch", "Automatic Transfer Switch (ATS)", "Bypass Isolation ATS"];
48    decimal Discount = [0..100];
49}
50
51type PowerUnit;
52type ControlSystem;

Key Technical Details 

  • Supertype as relation target: The accessories : Accessory relation accepts any subtype instance, including Heater, ExhaustBundle, or MaintenanceKit, because all three extend Accessory. The engine resolves which subtype is present at runtime. To target a specific subtype in a rule, reference it by name in the cardinality expression, for example accessories[Heater].
  • Cross-relation require(): require(controlSystems[ControlSystem] > 1, accessories[Heater]) evaluates a condition on one relation (controlSystems) and enforces a requirement on a different relation (accessories). The condition and the result do not have to involve the same relation or type.
  • Supertype cardinality in require(): require(accessories[Accessory], generators[GeneratorSet]) uses the supertype Accessory in the condition to check whether any accessory of any subtype is present. This is broader than checking for a specific subtype and fires regardless of which Accessory subtype the user selects.
  • require() and exclude() in the same model: GeneratorBundle uses require() to mandate components; ExhaustBundle uses exclude() to block incompatible combinations. Both rules coexist in the same model without conflict because they apply to different types and relations.
  • Multi-level hierarchy: ExhaustFilter : Filter and Heater : Accessory each inherit from their respective parent types. The filters : ExhaustFilter relation in ExhaustBundle targets the concrete subtype directly rather than the parent Filter, which constrains the relation to only that subtype’s instances.