Sibling, Parent, Root: How to Reference Attributes Across a CML Model

Cross-level attribute references are one of the quieter design choices in CML – and one of the ones consultants disagree on the most.

Two architects can look at the same bundle and propose two very different solutions for the same requirement: one wraps the dependency into a bundle-level constraint, the other proxies the value down through parent().

Both work. Both produce the same configurator behavior. And only one of them stays clean once the bundle starts growing.

This article walks through the four mechanisms CML gives you for referencing an attribute outside the current type – parent(), root(), sibling(), and relation traversal – and where each one earns its place. 

Summary 

  • CML organizes types in a hierarchy, and almost every non-trivial bundle eventually needs an attribute on one type to depend on an attribute on another.
  • Four mechanisms cover most of the cases: parent() for ancestors, root() for the top of the bundle, sibling() for peers in the same relation, and dot-notation traversal for reading children from a parent.
  • The common consultant reflex – placing a bundle-level constraint that compares attributes across types – is correct but often adds avoidable indirection. Proxy variables can express the same intent without involving the solver.

Why cross-level references come up at all

A CML model is a tree. A bundle sits at the top, relations hold its child components, and each child can have its own relations and so on. The structure is clean on paper – every type owns its own attributes, every relation defines its own cardinality, every constraint lives where the data lives.

Real configurations rarely stay that tidy.

A region attribute on the bundle has to drive which laptops are visible. A compliance flag on the root has to ripple down to every sensor. The price tier selected at the top has to be readable by a warranty three levels down.

Once a single attribute has to be consistent across types in the Advanced Configurator, you need a way to point from one type to another – and CML gives you several. The CML User Guide calls them proxy variables: special functions that let a variable on one type read a variable on another.

The four ways to reach across the model

Before looking at trade-offs, here is what each mechanism does at a glance. The examples below are all built around the same simple bundle so the differences are easier to see.

The starting model:

define REGION ['EU', 'UK', 'NA', 'APAC'];

 

type Laptop;

type Accessory;

type Warranty : Accessory;

type Printer;

type PrinterPaper;

 

type PrinterBundle : Accessory {

    relation printers     : Printer[1..1];

    relation paper        : PrinterPaper[0..5];

}

 

type LaptopProBundle {

    string region = REGION;

    relation laptops      : Laptop[1..3];

    relation accessories  : Accessory[0..10];

}

The region attribute on LaptopProBundle needs to be visible to the Laptop type one level down, and to the Printer type two levels down inside a PrinterBundle. The same value, three places.

1. parent() – read from the immediate ancestor

The parent() proxy variable pulls an attribute from the immediate parent type. A child type declares a local variable and assigns it the parent's value:

type Laptop {

    string region = parent(region);

}

No constraint, no comparison – the child literally inherits the value. An optional second argument, parent(region, 1), lets you skip past the immediate parent to a specific ancestor.

2. root() – read from the top of the bundle

When a value sits on the bundle root and a deeply nested type needs to read it, walking up level by level with parent() works but requires every intermediate type to carry a passthrough attribute. root() jumps straight to the top:

type Printer {

    string region = root(region);

}

No need to add a technical bundleRegion on PrinterBundle just to forward the value. The grandchild reads it directly.

3. sibling() – read from a peer in the same relation

Both parent() and root() move vertically – up the tree. There are scenarios where the value you need lives on a sibling: another instance in the same relation, on the same level. 

Walking up and back down works, but it adds a technical attribute on the shared parent for no functional reason. The sibling() proxy variable targets exactly this case.

Its signature is:

sibling(siblingAttributeName, currentInstanceIndex, offsetIndex)

It lets one type read an attribute from another instance in the same relation by matching index values. A small example: a PrinterBundle needs to know the level of the Warranty sitting next to it under the same Accessories relation.

type Warranty : Accessory {

    int index = 1337;

    string level = "Gold";

}

type PrinterBundle : Accessory {

    int index = 0;

    string warrantyLevel = sibling(level, index, 1337);

    // warrantyLevel gets the value of `level` from the Warranty instance

}

One important constraint to be aware of: sibling() has access only to instances that belong to the same relation. It does not walk across branches or levels – it operates strictly within the peer group. That is also its main advantage: the reference stays local and does not depend on the structure above.

Relation traversal – read from children, not from ancestors

The three mechanisms above move upward or sideways from a child's perspective. The fourth pattern reverses the direction: a parent type reads attributes on its own children, through the relation. Dot notation on the relation name does the work:

type LaptopProBundle {

    string region = REGION;

    int totalAccessories = accessories[Accessory].count();

    constraint(accessories[Accessory].region == region);

}

This is where most of the cross-level constraints live in real models – the parent expressing a rule that involves its children's attributes. parent() and root() are not interchangeable with traversal, because the data has to flow downward in one and upward in the other.

The pattern most teams default to – and where it costs

On our CML health checks, the dominant pattern we see for region-style synchronisation is a constraint placed on the bundle:

type LaptopProBundle {

    string region = REGION;

    constraint(laptops[Laptop].region == region);

    constraint(accessories[PrinterBundle].printers[Printer].region == region);

}

It works, and the configurator behaves correctly. But three things happen quietly:

  • The dependency is described as a comparison, not as inheritance. Logically the child's region is the parent's region. A constraint forces the solver to evaluate equality every time, even though there is no real choice involved.
  • All the cross-type logic concentrates on the bundle. Anyone reading the Printer type has no signal that its region is constrained from above – the rule lives somewhere else.
  • Refactoring the hierarchy gets harder. If a new intermediate type is introduced between PrinterBundle and Printer, every bundle-level constraint that reaches down has to be updated.

The CML Best Practices recommend keeping constraint logic close to the attributes it targets. Proxy variables do exactly that: they move the reference into the type that owns the attribute, and the bundle goes back to being a structural definition rather than a rules registry.

The cleaner alternative: read, do not compare

The rewrite of the same model using proxy variables looks like this:

type Laptop {

    string region = parent(region);    // immediate parent

}

 

type Printer {

    string region = root(region);      // top of the bundle

}

 

type LaptopProBundle {

    string region = REGION;

    relation laptops     : Laptop[1..3];

    relation accessories : Accessory[0..10];

}

The change is small, but the implications are not:

  • Locality. The rule about a Printer's region now lives in the Printer type. Anyone reading the printer code sees, in one line, where its region comes from.
  • Fewer constraints. The bundle no longer carries equality checks for every level of the hierarchy. Constraints are reserved for actual decisions the solver has to make.
  • Cheaper refactor. Add another intermediate type tomorrow and the Printer's root(region) still resolves correctly. The reference is anchored to the structure, not to a specific path.

There is also a subtle performance angle. As covered in our article on variable domains in CML, every constraint widens the work the solver has to do. A proxy variable assignment does not – it is a direct inheritance, evaluated once. On a large bundle, the difference between dozens of equality constraints and a few proxy variables is measurable in the debug log.

Side effects worth knowing about

Every mechanism here comes with side effects. Three of them matter in practice.

Readability

Proxy variables put the reference where the attribute lives, which is usually clearer for the developer working on that type. But a reader who only sees the bundle no longer has a single place to scan for every cross-level rule. Pick a convention – ideally per-type proxy variables, with bundle-level constraints reserved for genuine decisions – and stick to it.

Refactor cost when the hierarchy changes

parent() with no level argument is brittle: it reads from whatever turns out to be the immediate parent at runtime. Inserting a wrapper type between parent and child silently changes what parent() resolves to. root() is more stable in this respect because it always targets the top, regardless of intermediate types. 

Where stability matters – for example, in a generic component reused across bundles – root() is usually the safer call.

How the solver sees each pattern

Proxy variables resolve to direct value assignments – the solver does not need to evaluate them as constraints. Relation traversal inside a constraint, by contrast, becomes part of the search the solver performs, because the constraint may evaluate differently as cardinality changes.

The takeaway: prefer proxy variables for inheritance, reserve traversal for the cases where the parent genuinely needs to inspect or aggregate over its children.

A short decision rule

If you want one mental shortcut to take from this article, here it is.

Scenario Reach for Why
Child needs a value from its immediate parent parent(attr) Direct, single-hop reference; no intermediate plumbing
Grandchild needs a value from the bundle root root(attr) One call, no technical attributes on intermediate types
Child needs to skip past the immediate parent to a specific ancestor parent(attr, n) Explicit level index keeps the reference tied to hierarchy depth
A type needs a value from a sibling in the same relation sibling(attr, idx, target) Avoids walking up and back down through the tree
Parent needs to read or aggregate values from its children relation traversal
(children.attr / count / sum)
References flow downward through the relation, not through proxy variables
Two attributes on different branches must always match constraint at the
closest common
ancestor
Use a constraint only when no proxy variable can express the dependency

CML proxy variables: limitations to plan around

Three cases sit outside what parent(), root(), and sibling() can express – and recognising them early saves a refactor later.

  1. Bidirectional dependencies do not fit this model.

Proxy variables enforce a one-way flow – child reads from parent, never the other way around. If a value has to update both directions when either side changes, a constraint is the right tool and is unavoidable. The CML User Guide describes parent() explicitly as unidirectional data flow, and that is by design.

  1. Group types behave differently.

As noted in the official documentation, the parent keyword is not supported inside a Group Type. If a component lives inside a PCM-driven product component group, plan the references accordingly.

  1. Same variable name across levels is a convention, not a requirement.

Reusing region at every level keeps the code readable, but the proxy variable resolves by argument, not by matching name. Pick one and stay consistent.

Final thoughts

Choosing between a constraint and a proxy variable is rarely about correctness – both will pass UAT. It is about who reads the code six months later, what happens when the bundle grows another level, and how much the solver has to do every time a sales rep changes a selection.

Proxy variables are an underused tool in CML.

Most models we review during CML health checks lean heavily on constraints for cross-level synchronisation, even where a single parent() or root() would say what the model actually means. When the bundle starts to feel like a rules registry rather than a structural definition, that is the signal to look at proxy variables.

FAQ: CML proxy variables

When should I prefer parent() over a constraint?

Whenever the child's value is the parent's value – inheritance, not comparison. If both sides could legitimately change and the engine has to reconcile them, that is a constraint. If the child should simply track the parent, parent() expresses the relationship more directly.

Does using root() bypass the parent chain?

root() reads directly from the top of the bundle, regardless of how many intermediate types sit in between. That is the point: a deeply nested grandchild does not need every layer above it to forward the value.

Is parent(attr, 1) the same as root(attr)?

Only when the grandparent is also the bundle root. parent(attr, 1) walks exactly one level above the immediate parent. root(attr) walks to the top, however far that is. The two coincide in a two-level model and diverge as soon as the hierarchy grows.

Can proxy variables introduce circular dependencies?

By design, no. The CML User Guide calls out unidirectional data flow as one of the reasons parent() exists – children read from parents, never the other way around. Circular dependencies are typically a sign that a constraint is being used where a proxy variable would do the job.

Where can I read more about CML hierarchy patterns?

The official starting point is the Using Proxy Variables with Constraints on Types and Relationships section of the Revenue Cloud Developer Guide. On the Veloce side, the articles on variable domains and CML performance issues cover the patterns where cross-level references most often hurt performance.

Latest Blog Posts
Ready to accelerate your revenue growth?