Table Constraints in CML: Syntax, Real Use Cases, and Pitfalls

If you have worked with the Advanced Configurator in Salesforce Revenue Cloud ( now officially Agentforce Revenue Management) for any length of time, you know the feeling: your CML model starts clean, but as the product catalog grows, so does the number of constraint rules. 

Every new item-category rule, every channel-specific discount cap, or every region-city dependency adds another line of logic. Before long, you are maintaining hundreds of individual constraints that all follow the same pattern – mapping one attribute value to another.

Table Constraints exist to solve exactly this problem.

In this article, we will walk through the concept, show you the CML syntax, explore four real-world use cases from production implementations, and highlight the limitations and gotchas that can trip you up if you are not prepared.

Summary 

  • Table Constraints turn repetitive constraint logic into a single readable structure – less code, fewer bugs, easier audits.
  • SalesforceTable gives business users direct control over constraint data without touching CML or waiting for deployments.
  • Hardcoded tables fit stable engineering rules, SalesforceTable fits everything the business owns – knowing which to use saves you from rewriting later.
  • Four silent failure modes covered: missing permissions, field name typos, data type mismatches, and decimal precision conflicts between Salesforce and CML.

What is a Table Constraint?

Introduced with the Advanced Configurator in Summer '25, a Table Constraint is a type of CML constraint that defines a set of valid combinations for two or more attributes. 

Instead of writing a chain of implication constraints (if A then B, if C then D, and so on) or in a single large boolean expression, you define them all in one place: a table. Each row is one allowed combination. Anything not in the table is automatically invalid. The constraint engine reads the table and enforces it as a single unit.

Table Constraint in practice

Consider a simple example: you are configuring a product that has a Color attribute and a Size attribute, but not every size is available in every color:

  • a red t-shirt comes in S, M, and L
  • a black t-shirt comes in M, L, and XL
  • a green t-shirt is only available in L.

Without Table Constraints, you would need to write individual constraint for each combination:

// Without Table Constraints — one constraint per combination

constraint(Color == "Red" -> Size == "S" || Size == "M" || Size == "L");

constraint(Color == "Black" -> Size == "M" || Size == "L" || Size == "XL");

constraint(Color == "Green" -> Size == "L");

With a Table Constraint, the same logic is expressed as a single table:

// With Table Constraint — all combinations in one place

constraint validCombinations(

  table(

    Color, Size,

    {"Red", "S"}, {"Red", "M"}, {"Red", "L"},

    {"Black", "M"}, {"Black", "L"}, {"Black", "XL"},

    {"Green", "L"}

  ),

  "This color-size combination is not available."

);

That difference becomes dramatic at scale. Take 3 colors and 3 sizes – that is up to 9 combinations, only 7 are valid. Now imagine 50 options and 20 values: that is dozens of if/then blocks to write, test, and maintain. With a table, it stays one constraint – regardless of how many combinations exist.

Table Constraint syntax in action

The general syntax for a Table Constraint in CML is:

table(variable1, variable2, ..., variableN,

  {value1, value2, ..., valueN},

  {value1, value2, ..., valueN},

  ...

);

Each row inside curly braces defines one valid combination. The variables listed as columns correspond positionally to the values in each row. You can include an error message as the second argument to the constraint for user-facing feedback when a violation occurs.

Key benefits of Table Constraints in CML

Table Constraints sit at the intersection of two worlds. For the technical team, they simplify the constraint model itself – fewer constraints, better performance, cleaner architecture.

For the business side, especially when paired with SalesforceTable, they unlock something that is rare in configurator implementations: the ability to change product logic without touching code. That is why the advantages split naturally into two groups.

For developers and architects

  • Reduced code volume. One Table Constraint can do the job up to hundreds of individual constraints. That means less code to review or debug, and fewer places where something can quietly break.
  • Improved performance. The engine evaluates a table as one unit, not as a pile of separate constraints. In complex models with lots of attribute dependencies, that can noticeably cut down on solver cycles and backtracking.
  • Named constraint support. You can give a Table Constraint a name and apply it conditionally – for example, activating different tables for different business scenarios without duplicating logic.

For business users and admins

  • Self-service data management. When using SalesforceTable, the data behind your constraints lives in a regular Salesforce object. Business users edit it the way they edit any other record. No CML knowledge required.
  • Mass updates. Say you need to change discount caps across 200 products. With SalesforceTable, that is one Data Loader job (read further). With hardcoded constraints, the same change requires manual edits to 200 lines of CML.
  • No code changes needed.  When you update records in a SalesforceTable object, the CML code stays untouched – you just reactivate the model for the engine to pick up the new data.

The advantages above hold for both approaches to Table Constraints – hardcoded in CML and dynamic via SalesforceTable. But which one should you actually use? The answer depends on who owns the data and how often it changes.

Static tables vs. SalesforceTable: choosing the right approach

CML supports two ways to populate a Table Constraint: hardcoded rows directly in CML, or dynamic data imported from a Salesforce object using the SalesforceTable keyword. So, what’s the difference?

Hardcoded tables

Hardcoded tables are defined entirely within CML. The data is part of the constraint model itself. This approach is appropriate when the combinations are stable and rarely change – f.e., technical compatibility rules between hardware components where the valid pairings are determined by engineering and do not shift with business conditions.

This Voltage/DutyRating example is a classic case:

type GeneratorSet {

  string Voltage = ["220/380", "277/480", "7976/13800"];

  string DutyRating = ["Prime Power (PRP)",

                       "Continuous Power (COP)",

                       "Emergency Standby Power (ESP)"];

  constraint validOperationalModes(

    table(

      Voltage, DutyRating,

      {"220/380", "Prime Power (PRP)"},

      {"220/380", "Continuous Power (COP)"},

      {"220/380", "Emergency Standby Power (ESP)"},

      {"277/480", "Prime Power (PRP)"},

      {"277/480", "Emergency Standby Power (ESP)"},

      {"7976/13800", "Continuous Power (COP)"}

    ),

    "Selected Voltage is not compatible with the

     required Duty Rating."

  );

}

This example is adapted from the Salesforce Revenue Cloud Developer Guide.

The downside is clear: any change to the valid combinations requires editing the CML code, deactivating the model, redeploying, and reactivating. For technical configurations that change annually or less, this is fine. For business-driven data that shifts quarterly or monthly, it is not.

SalesforceTable: dynamic, business-managed data

The SalesforceTable keyword allows you to pull constraint data from a standard or custom Salesforce object. The CML code references the object and its fields; the actual data lives in Salesforce records that can be updated through the standard UI, Data Loader, or any integration.

Syntax:

constraint(

  table(

    CML_Variable1, CML_Variable2,

    SalesforceTable("ObjectAPIName__c",

                    "Field1__c, Field2__c")

  ),

  "User-friendly error message"

);

The field names in the SalesforceTable call map positionally to the CML variables. The first field maps to the first variable, the second field to the second variable, and so on.

Note! Before SalesforceTable will work, you must assign Read, Create, Edit, and Delete permissions on the source object to the Constraint Rules Engine Licenseless permission set. Without this, the engine silently fails to load the data.

Here is a practical example: constraining a generator’s calculated running capacity based on the user’s selection of nominal power output, with the valid pairings stored in a custom object called PowerCst__c:

type GeneratorSet {

  string Nominal_Power_Output = 

    ["100 kW", "300 kW", "500 kW", "700 kW"];

  @(configurable = false, defaultValue = "0")

  decimal(2) gc_runningKw;

  constraint(

    table(

      Nominal_Power_Output, gc_runningKw,

      SalesforceTable("PowerCst__c",

                      "Nominal__c, Running__c")

    )

  );

}

Based on an example from the Salesforce Revenue Cloud Developer Guide.

The beauty of this approach is separation of concerns: the CML model defines the rule structure, while the Salesforce object holds the data. When business conditions change – a new power output tier is added, or running kW values are recalibrated – a business user or admin updates the Salesforce records. With no CML changes or developer needed.

When to use which

Criteria Hardcoded Table SalesforceTable
Data change frequency Rarely (annually or less) Regularly (monthly+)
Who maintains it CML developer Business user / admin
Deployment required for changes Yes (deactivate → edit → reactivate) No (update SF records)
Best for Technical/engineering rules Business-driven rules
Example Voltage ↔ Duty Rating Discount caps by channel

Table Constraint in CML: real-world use cases

The following use cases come from the Veloce team Revenue Cloud implementations. They illustrate scenarios where Table Constraints moved from a nice-to-have to a necessity.

Use Case 1: Dependent picklists 

A common requirement in global configurations is cascading selection: the user picks a country, and the available cities are filtered accordingly. Without Table Constraints, this means writing one implication constraint per country:

// Without Table Constraints

constraint(Country == "USA" -> City == "New York"

  || City == "Chicago" || City == "LA");

constraint(Country == "Germany" -> City == "Berlin"

  || City == "Munich" || City == "Hamburg");

constraint(Country == "Japan" -> City == "Tokyo"

  || City == "Osaka" || City == "Kyoto");

// ... repeat for every country

With a SalesforceTable, you create a custom object (for example, CountryCityMapping__c) with two fields – Country__c and City__c – and populate it with all valid country-city pairs. The CML constraint becomes a single statement:

constraint(

  table(

    Country, City,

    SalesforceTable("CountryCityMapping__c",

                    "Country__c, City__c")

  ),

  "This city is not available for the selected country."

);

When a new city is added to the business – let's say, a new office opens in Nagoya – an admin adds a record to the Salesforce object.

Use Case 2: Discount caps by sales channel

In multi-channel selling, different sales channels typically have different maximum discount thresholds. A distributor might get up to 15% off, a reseller also 15%, and a direct channel’s discount is 0%. These caps often vary by product as well.

In one project, the team stored all of this in a single Salesforce object (DiscountCaps__c) – one row per product, with a separate column for each channel. On the CML side, each channel gets its own named table constraint that only kicks in when that channel is active:

// Named table constraints per channel

constraint distributorTableCst = table(

  _productId, _discountCapPercent,

  SalesforceTable("DiscountCaps__c",

                  "Product__c, Distributor__c")

);

constraint(Channel_Type__c == "Distributor"

  -> distributorTableCst || _discountCapPercent == 100);

constraint resellerTableCst = table(

  _productId, _discountCapPercent,

  SalesforceTable("DiscountCaps__c",

                  "Product__c, Reseller__c")

);

constraint(Channel_Type__c == "Reseller"

  -> resellerTableCst || _discountCapPercent == 100);

Here is the pattern: each channel has its own named table constraint that maps products to their discount caps. The conditional logic (Channel_Type__c == "Distributor" ->) ensures that the table is only enforced when that channel is active. The fallback (|| _discountCapPercent == 100) handles products not present in the table.

The business value here is direct: when the commercial team decides to raise the distributor discount cap from 15% to 17%, they update one column in the DiscountCaps__c object.

Use Case 3: Product parameters combinations

Manufacturing and retail companies frequently deal with product variants where the available options depend on one another. A specific shoe model might come in sizes 7-12 in black, but only 8-11 in white. A furniture line might offer a sofa in fabric or leather, but leather is only available in three colors while fabric comes in fifteen.

Without Table Constraints, the constraint count grows multiplicatively. With three product lines, five colors, and eight sizes, you could easily need 40+ individual constraints. A single Table Constraint (ideally backed by SalesforceTable) replaces all of them:

constraint validVariants(

  table(

    ProductLine, Color, Size,

    SalesforceTable("ProductVariants__c",

      "ProductLine__c, Color__c, Size__c")

  ),

  "This combination is not available."

);

When the product team launches a new color, they add the records to the ProductVariants__c object, and the configurator immediately reflects the expanded options.

Use Case 4: Preset-like selection

A less obvious but powerful application of Table Constraints is preset configuration. Imagine a bundle where selecting a preset (for example, "Basic", "Standard", or "Premium") should automatically select or deselect specific products in the bundle.

Without Table Constraints, this requires writing separate constraints for each preset-product combination. As we note in our Advanced Configurator Audit best practices, even a relatively simple model requires quite a number of constraints to achieve preset-like selection. Table Constraints simplify this by providing a matrix showing which products should be selected or deselected for each preset value:

// Each row maps a preset to product on/off states

constraint presetConfig(

  table(

    Preset, IncludeWifi, IncludeBluetooth,

    IncludeGPS,

    {"Basic",    false, false, false},

    {"Standard", true,  true,  false},

    {"Premium",  true,  true,  true}

  )

);

Pitfalls and limitations of CML Table Constraints

Table Constraints save a lot of effort, but like any powerful tool, they have boundaries. Knowing them upfront can save you hours of debugging and prevent unpleasant surprises when your data scales beyond what worked in a sandbox.

  1. Permission configuration

SalesforceTable needs CRUD permissions on the source object – you grant them through the Constraint Rules Engine Licenseless permission set. Easy to forget, especially when migrating from sandbox to production.

If the permissions are missing, the table loads as empty and the constraint effectively does nothing – which can be very difficult to debug.

  1. Debugging table data

If a Table Constraint is not doing what you expect, start with the data. Check that the Salesforce object actually has the records you think it has, that values match CML attribute domains exactly (casing and whitespace matter), and that field API names in the SalesforceTable call have no typos.

A common mistake is referencing a field with a typo that produces no error message.

  1. Not a replacement for all constraints

Table Constraints are ideal for fixed attribute-to-attribute mappings. They do not work for dynamic calculations (f.i., "discount = base price * discount factor"), aggregation logic, or conditional constraints that depend on cardinality or relational context.

Use Table Constraints for what they do best: defining which combinations are valid. Use standard constraints, formulas, and aggregation functions for everything else.

  1. Data type alignment

The data types in the Salesforce object fields must be compatible with the CML variable types. A string field in Salesforce maps to a string variable in CML, a number field maps to an int or decimal. Mismatches can cause unexpected behavior.

Pay particular attention to decimal precision – if your CML variable is decimal(2) but the Salesforce field stores four decimal places, the engine may not find a match.

Final thoughts

Table Constraints are one of the most underutilized features in CML. With it you can achieve something rare in enterprise software: genuine self-service configuration.

The concept is easy to apply – define valid combinations in a structured format – but the impact on code quality, maintainability, and business empowerment is significant.

Many implementation teams discover them late, after they have already built hundreds of individual constraints that could have been a single table. If this is your case, welcome to check how the Veloce team can help

FAQ: Table Constraints in CML  

How many rows can a Table Constraint support?

A known limitation in version 260.x caps the actual loaded rows at 10,000. If your data set is larger, consider splitting it across multiple Salesforce objects or using conditional table constraints to load only the relevant subset.

Can I combine a Table Constraint with regular constraints?

Yes. Table Constraints can coexist with standard implication constraints, exclude rules, require rules, and any other CML construct. They can also be named and used conditionally, as demonstrated in the discount caps example.

Do I need to redeploy CML when I update SalesforceTable data?

Yes. You need to reactivate the CML after updating SalesforceTable data. During model activation, the table data is cached to enable fast retrieval and execution at runtime. As a result, any changes in the Salesforce object (such as adding, updating, or deleting records) will only take effect after the model is reactivated.

What data types are supported in Table Constraints?

Table Constraints support string, integer, decimal, and boolean values. The types in the Salesforce object fields must match the CML variable types – otherwise the mapping breaks.

Can I use Table Constraints with the Visual Builder, or only with CML Editor?

Only CML Editor. The Visual Builder handles basic constraints, but Table Constraints and SalesforceTable require writing CML directly.

Latest Blog Posts
Ready to accelerate your revenue growth?