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.
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.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.
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.
parent() – read from the immediate ancestorThe 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.
root() – read from the top of the bundleWhen 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.
sibling() – read from a peer in the same relationBoth 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.
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.
.png)
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 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 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:
Printer's region now lives in the Printer type. Anyone reading the printer code sees, in one line, where its region comes from.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.
.png)
Every mechanism here comes with side effects. Three of them matter in practice.
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.
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.
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.
If you want one mental shortcut to take from this article, here it is.
Three cases sit outside what parent(), root(), and sibling() can express – and recognising them early saves a refactor later.
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.
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.
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.
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.
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.