

.png)
A slow configurator costs deals – the rep waits, the customer drifts, the quote stalls. The usual suspect is data volume, but more often it's one setting left undefined in how the bundle was modeled: cardinality.
It does two jobs at once: it sets a business rule and tells the engine how hard to work on every quote. Most developers set the first and forget the second – and that's where the lag comes from.
Let's break down how it works, what [1..1], [0..n], and [1..n] each mean, and the mistakes that show up most in CML health checks.
[min..max] bound that says how many child instances may exist. Together they describe the shape of a bundle before any constraint is written.[1..1] resolves in one step. [0..5] tests a handful of states; an unbounded relation tests quantities all the way to 9,999.CML is the language behind the Advanced Configurator in Agentforce Revenue Management (formerly Revenue Cloud Advanced), generally available since Summer ’25. It is constraint-based rather than rule-based: instead of scripting the steps to build a valid configuration, you describe the valid end state and let the constraint engine work out the rest.
A model is built from a few primitives. Types stand in for products and product classes. Variables hold their attributes. Relationships connect a parent type to the child types it can contain – a bundle to its operating system, a server rack to its cooling modules. The relation keyword names that link.
Cardinality is the number attached to the relation.
It sets how many instances of the child type the configuration may, or must, hold. The syntax is [min..max]: [1..1] for exactly one, [0..1] for none or one, [1..10] for between one and ten. A single value like [2] fixes an exact count. Leave the bracket off entirely and the relation falls back to the engine’s default, which is where most of the trouble starts.

A single example carries the whole range better than four disconnected ones. Here is a SaaS platform bundle that uses each pattern once:
type SaaSPlatformBundle {
relation plan : SubscriptionPlan [1..1];
relation userLicenses : UserLicense [1..500];
relation support : SupportTier [1..1];
relation sso : SSOIntegration [0..1];
relation training : TrainingPackage [0..5];
relation storage : StorageAddOn [0..10];
}
type SubscriptionPlan;
type UserLicense;
type SupportTier;
type SSOIntegration;
type TrainingPackage;
type StorageAddOn;
plan and support. Every sale has to include one subscription plan and one support tier, never zero and never two. The engine resolves each in a single step – there is no quantity to search, only a value to assign. Reach for [1..1] whenever the user must pick exactly one and the component cannot repeat. Cost to the solver: negligible.
userLicenses [1..500]. A seat count is mandatory – a platform with no users is not a sale – but the quantity varies. The upper bound of 500 reflects what this tier actually sells, not a round number picked for safety. That single bracket also stands in for two constraints you would otherwise write by hand:
constraint(userLicenses[UserLicense] > 0);
constraint(userLicenses[UserLicense] <= 500);
With [1..500], both disappear. Use this when a component is always present but variable in count, and set the maximum to the real business ceiling. [1..9999] behaves the same as no bound at all. Cost to the solver: proportional to the range – keep it tight.
sso. Single sign-on is an add-on some accounts want and most skip, and no one needs two of. The engine checks exactly two states: present or absent. If the question a rep is answering is simply “do you want this, yes or no,” the bound is [0..1]. Cost to the solver: two states, resolved in one pass.
training [0..5] and storage [0..10]. Neither is required, both can stack. A customer might buy several training packages for different teams, or add storage blocks as data grows. As with [1..n], the bracket replaces the manual quantity constraints and keeps the quantity rule where it belongs – on the relation, not scattered through the constraint block.
Always set a realistic ceiling. Cost to the solver: low when n is small, and tightening loose [0..n] relations is usually the single highest-impact change in a health check.
Write relation storage : StorageAddOn; with no bracket and the engine treats the quantity as open-ended. The official guidance is explicit on what that means: if you don’t specify cardinality, the constraint engine tries to set a quantity of 1, 2, 3, all the way up to 9,999, while a [0..1] bound lets it test far fewer combinations. For a component that tops out at ten, that is 10,000 quantities tested to rule out 9,990 that could never apply. Across six relations, the cost compounds.
Two things make this the most common cardinality mistake:
require constraints that the cardinality would have handled in one line – more code, more places to get it wrong, logic split between the relation and the constraint block. Treat every auto-generated relation as a line that still needs its bound.The Advanced Configurator runs a constraint solver. To resolve a configuration, it works through three steps on every relation:
For a relation with no explicit bound, that loop runs across every quantity from zero to the default ceiling – which is why an unbounded relation shows up in the debug log as a long stretch of wasted search.
Cardinality is the cheapest performance lever in the model.
The CML Best Practices put it plainly: specify the smallest required cardinality, so the engine avoids testing values it never needed to consider. Dropping an optional accessory from the open default to [0..5] cuts the quantities tested for that relation by more than three orders of magnitude. On a bundle with ten or more relations, those reductions stack into a configurator that resolves in under a second instead of dragging.
A short example shows the scale. A hardware reseller bundle had twelve component types imported from PCM, none with cardinality:
The fix wasn't a rewrite – just bounds added and the now-redundant require constraints removed.
In our experience, the same handful of cardinality problems recur regardless of industry or team size.
The default. Almost always inherited from generated output and left in place. This is the first thing worth scanning for, because it is both the most common and the easiest to fix.
[0..9999] written deliberately, or [1..1000] on a component that never sells past ten. A wide range is sometimes legitimate – if the business genuinely allows 1 to 1,000 licenses, you cannot fake a tighter number – but everything that does have a real limit should carry it.
A require rule forcing a quantity above zero, where a [1..n] bound would say the same thing in the relation itself. When a new requirement comes up, check first whether changing the relation’s cardinality covers it; the distinction between enforcing and recommending a selection is its own topic, covered in our article on setDefault vs require.
It isn’t. [0..1] on two separate options does not make them mutually exclusive – that still needs a constraint. Cardinality is static structure – runtime logic that depends on what the user selected lives in constraint expressions.
The throughline is that cardinality is a modeling decision, not a default to accept. Every relation in a model is making a statement about how many of something a bundle can hold.
The question worth asking on each one is whether that statement matches the business – and whether the engine has been told the smallest version of it that is still true. It sits alongside the same instinct behind variable domains, where bounding an attribute’s range does for values what cardinality does for quantities.
Cardinality sits in an awkward spot: too small to feel like architecture, too consequential to leave to whatever the import tool produced.
The bound is the first thing the solver reads about a relation and the last thing most developers think to check. Setting it deliberately costs a few seconds per relation and saves the engine work on every configuration that follows.
If a once-instant configurator starts to lag, check the relations first. Go through the model and make sure every bound is as tight as it can be without breaking a real rule. It's a small habit that prevents most cardinality-related slowdowns before they reach production.
What happens if I don’t set cardinality on a relation?
The engine applies its default and tests quantities from 1 up to 9,999 for that relation, on every configuration. It works, but it is the most common source of avoidable solver load – and it is usually inherited from auto-generated output rather than chosen.
Is [1..9999] the same as leaving the bound off?
Functionally, close to it. Both force the solver to consider a very wide quantity range. If a component has a real upper limit, set it; a tight maximum is the whole point of the bound.
Does cardinality replace constraints?
For quantity rules, often yes – [1..500] says what two require/limit constraints would. For anything else, no. Mutual exclusivity, conditional selection, and cross-component logic still need constraints. Cardinality describes structure, not behavior.
Can I set an exact count instead of a range?
Yes. A single value such as [2] fixes the quantity at exactly two, which is [2..2]. Useful when a component always comes in a fixed multiple.
My configurator got slow as the catalog grew. Could cardinality be the cause?
Frequently. Performance from unbounded relations is invisible at small scale and compounds as the model grows, which is why it passes early testing and surfaces later. Checking that every relation carries a realistic bound is one of the first diagnostics worth running.
Where’s the official reference?
The CML Best Practices page in the Revenue Cloud Developer Guide covers cardinality and the smallest-required-bound guidance. This article goes past the reference to show what each choice costs in a live model.