When to Use
Worked recipes for repeating a card over a multi-reference field and over a GROUP-MULTIPLE field, with the full node trees.
Part of the API-authoring set. Start at author-composition-via-api, which routes to this one.
Recipe: Repeat a Card Over a Multi-Reference Field
The canonical pattern. Requires FIVE shapes in concert. Missing any one yields a silently-empty repeater or (worse) an all-items-identical render (the template-at-.0. trap below).
Scenario: a category template (Connected to the category CT). Each category entry has a products multi-reference field. We want to render one card per referenced product.
1. Composition-level: data_sources.resolvedReferences
The reference-resolution map. Tells the SDK “when bindings resolve through this reference path, materialize each stub into a real entry”:
"data_sources": [
{
"uid": "template",
"data": null,
"resolvedReferences": {
"template": ["products"]
}
}
]
Example: a section (Card Grid) that iterates related_posts:
"data_sources": "[{\"uid\":\"template\",\"data\":null,\"resolvedReferences\":{\"template\":[\"related_posts\"]}}]"
Wire format quirk: on the CMA data_sources is stored as a JSON-encoded string ("[]", "[{…}]"), not the parsed array. Serialize before write. Parse after read.
Key semantics: depends on the data source, and getting it wrong fails silently:
| Data source | Key | How the SDK reads it |
|---|---|---|
| template (the page entry) | Opaque label, Studio writes "template" | Flattens Object.values(resolvedReferences), key content is ignored |
| contentstack (pinned entries) | Must be the entry uid | Direct lookup resolvedReferences[entry.uid], a key that doesn’t match the entry falls through to [] |
Values are the field paths on the parent entry in both cases.
So the “key doesn’t matter” rule is template-scope only. For pinned entries the key is a lookup, not a label: mis-key it and the SDK finds no paths, adds no includes, and the references stay stubs, with no error, exactly as if you had never set resolvedReferences at all.
Without this, reference fields are just [{uid, _content_type_uid}, …] stubs at runtime. The iteration sees no items.
Where a reference path must terminate: the four shapes
One rule, four instances. A resolvedReferences value becomes a CDA include[] entry, so it must name a real, fetchable field chain ending at the reference (or at the nested field you need resolved). Include every intermediate level, and no segment that isn’t a field.
| Source shape | Path to write | Not |
|---|---|---|
| Multi- or single reference on the parent | products | - |
| Reference inside a Modular Block | blocks.<block_uid>.<ref_field> | blocks.<ref_field>, block uid omitted |
| Reference inside a reference | carousel.cards | carousel.products_carousel.cards, CT uid leaked |
| Nested asset inside a reference | both hero and hero.image_options.image | hero alone, scalars resolve, the asset doesn’t |
| Reference inside a group-multiple | footer_columns.related_entries | - (groups add no extra level) |
Why this matters: every violation fails the same silent way. A path that doesn’t name a fetchable chain would 422 the CDA query, so Studio filters it out before the query is sent rather than letting it error. Nothing warns you. The reference stays a stub, the binding resolves against that stub, and the component renders empty. A correct-looking binding with missing data is the signature of a bad path, not of a bad binding.
Two segment types are not fields and must not appear:
- Array indexes (0, 2) and the entries array-wrapper: the SDK strips both.
- Reference-target CT uids. A reference-inside-a-reference produces the inner reference’s target CT uid directly after the reference field (carousel, then products_carousel, then cards). Studio drops it positionally: a segment is a discriminator iff it is a known CT uid and the preceding segment is a reference field whose reference_to includes it. That precision is deliberate: the same uid is often also a modular-block uid elsewhere in the path, and a block (having no preceding reference) is correctly preserved.
Numeric prefixes on block segments (2_feature_block) are stripped by both Studio and the SDK, so write the plain uid.
2. Repeater node’s items binding: at props.items.binding, type template
The Repeater consumes its iteration source through its items prop:
{
"type": "repeater",
"uid": "R1",
"props": {
"items": {
"binding": {
"type": "template",
"value": { "path": { "products": {} } }
}
},
"children": { "type": "slot", "slot": "R1-slot" }
},
"metadata": { "mode": "preview" },
"slots": { "R1-slot": [ /* see step 4 */ ] }
}
props.children is required: it wires the R1-slot slot to the Repeater’s built-in children prop. Without it the Repeater iterates but renders nothing (§ Node anatomy point 4). The slots key MUST match props.children.slot.
For a section whose selectedField is the reference itself (build-section‘s scope-aware sections / P22-P24), the path collapses to scope-root { "path": {} }, the section’s dataSources.template is already the resolved array.
3. Repeater node’s metadata.mode: "preview"
On the same Repeater node, in metadata (parallel to props). Per use-repeater‘s two-mode model: Design Mode (default) renders one placeholder. Preview Mode renders N real iterations. UI authors toggle this in Properties, under Configuration. API authors set the metadata directly. See the Repeater node shape in step 2 above for placement.
4. Condition Block as the immediate child of the Repeater (under slots)
Required by Studio’s composition schema for reference iteration (single-CT AND multi-CT, use-condition-block). Narrows the iteration item to a specific content type before child bindings resolve.
The Condition Block lives under a slot of the Repeater. Its discriminator is in metadata.condition:
{
"type": "repeater",
"uid": "R1",
// …items binding / metadata.mode as in step 2…
"props": {
"items": { "binding": { /* …as step 2… */ } },
"children": { "type": "slot", "slot": "<repeater-slot-uid>" } // ← wire the Repeater's slot
},
"slots": {
"<repeater-slot-uid>": [
{
"type": "condition-block",
"uid": "CB1",
"metadata": {
"condition": { "type": "reference", "value": "product" }
},
"props": {
"children": { "type": "slot", "slot": "<cb-slot-uid>" } // ← Condition Block needs it too
},
"slots": {
"<cb-slot-uid>": [
/* card subtree here — step 5 */
]
}
}
]
}
}
For modular-block iteration: metadata.condition.type: "modular_block", value: "<block-uid>".
Both container nodes carry props.children pointing at their own slot (§ Node anatomy point 4). The Condition Block also exposes a children slot prop. Skip its pointer and the card subtree never renders even though the Repeater iterates correctly.
5. Card prop bindings: type repeater, with repeaterUID
The “all cards identical” trap: Do NOT use template bindings indexed at .0.<field> (e.g. products.0.title). A fixed .0. index does NOT get substituted by the iteration: every iteration renders item 0. This is what bit P19.
Use repeater-scope bindings: type: "repeater", with repeaterUID naming the parent Repeater node, and a path relative to the iteration item (NO index). The card node lives inside the Condition Block’s slot:
{
"type": "site-product-card", // the registered component's type
"uid": "CARD1",
"props": {
"title": {
"binding": {
"type": "repeater",
"value": {
"repeaterUID": "R1",
"path": { "title": {} }
}
}
},
"image": {
"binding": {
"type": "repeater",
"value": {
"repeaterUID": "R1",
"path": { "images": { "0": { "url": {} } } }
}
}
},
"price": {
"binding": {
"type": "repeater",
"value": { "repeaterUID": "R1", "path": { "price": {} } }
}
}
}
}
The repeaterUID (“R1” here) tells the SDK at render time which Repeater’s current item this binding reads from. The path is on the iteration item itself.
Assembled
Composition entry
├── composable_uid: "<15-char>" (= CMA entry uid)
├── data_sources: [{ uid:"template",
│ resolvedReferences:{ "template":["products"] } }]
└── ui (zlib-inflated tree)
└── Section "ProductGrid"
└── Box (grid layout — register-component § Layout contract)
└── { type:"repeater", uid:"R1",
metadata:{ mode:"preview" },
props:{ items:{ binding:{ type:"template",
value:{ path:{ products:{} } } } } },
slots:{ "<R1-slot>":[
{ type:"condition-block", uid:"CB1",
metadata:{ condition:{ type:"reference", value:"product" } },
slots:{ "<CB1-slot>":[
{ type:"site-product-card", uid:"CARD1",
props:{
title:{ binding:{ type:"repeater",
value:{ repeaterUID:"R1", path:{ title:{} } } } },
image:{ binding:{ type:"repeater",
value:{ repeaterUID:"R1", path:{ images:{ 0:{ url:{} } } } } } },
price:{ binding:{ type:"repeater",
value:{ repeaterUID:"R1", path:{ price:{} } } } }
} }
] } }
] } }
Five shapes: composition resolvedReferences + Repeater props.items.binding + Repeater metadata.mode + Condition Block in slot with metadata.condition + repeater-scope card bindings. Any one missing produces an empty render or an all-identical render.
Recipe: Repeat a Card Over a GROUP-MULTIPLE Field
Sibling to the multi-reference recipe above, but simpler: group-multiple values are inline on the parent entry, so no reference resolution is needed.
Scenario: a category entry has a features field of type group + multiple: true (a list of inline objects, each with sub-fields like headline, body, icon). Render one card per feature.
What’s different vs the multi-reference case
| Shape | Multi-reference | Group-multiple |
|---|---|---|
| Composition data_sources.resolvedReferences | Required: reference stubs need resolving | NOT needed: values are inline on the parent entry |
| Condition Block as Repeater’s immediate child | Required: references are polymorphic (any allowed CT). The block narrows | NOT needed: group-multiple has one shape, not polymorphic |
| Repeater props.items.binding | type: "template", path at the multi-ref field | (same) type: "template", path at the group-multiple field |
| Repeater metadata.mode: "preview" | Required for canvas iteration | (same) Required |
| Card prop bindings | type: "repeater", repeaterUID, no index | (same) type: "repeater", repeaterUID, no index |
So group-multiple drops to THREE shapes (Repeater items binding + Repeater metadata.mode + repeater-scope card bindings). No composition-level resolved-references map. No Condition Block.
Group-multiple Repeater + card, assembled
Composition entry
├── composable_uid: "<15-char>" (= CMA entry uid)
├── data_sources: [/* no resolvedReferences entry needed */]
└── ui (zlib-inflated tree)
└── Section "FeatureList"
└── Box (flex / grid layout — register-component § Layout contract)
└── { type:"repeater", uid:"R2",
metadata:{ mode:"preview" },
props:{ items:{ binding:{ type:"template",
value:{ path:{ features:{} } } } } },
slots:{ "<R2-slot>":[
/* card goes here directly — no Condition Block wrapper */
{ type:"site-feature-card", uid:"FC1",
props:{
headline:{ binding:{ type:"repeater",
value:{ repeaterUID:"R2", path:{ headline:{} } } } },
body:{ binding:{ type:"repeater",
value:{ repeaterUID:"R2", path:{ body:{} } } } },
icon:{ binding:{ type:"repeater",
value:{ repeaterUID:"R2", path:{ icon:{ url:{} } } } } }
} }
] } }
The card sits directly inside the Repeater’s slot, no Condition Block in between, because the iteration item has one shape (the group’s sub-fields). Each card prop reads from the iteration item via repeaterUID:"R2" + the field path on the item.
Decision rule: which recipe to use
Look at the field’s data_type on the source CT:
| Field shape | Recipe to use |
|---|---|
| data_type: "reference", field_metadata.ref_multiple: true (multi-reference) | Multi-reference recipe, needs resolvedReferences + Condition Block |
| data_type: "reference", field_metadata.ref_multiple: false (single reference) | Multi-reference recipe minus the Repeater (a single Section drop, not iteration). The reference still needs resolvedReferences. |
| data_type: "group", multiple: true (group-multiple) | Group-multiple recipe, no resolvedReferences, no Condition Block |
| data_type: "blocks" (Modular Block) | Multi-reference recipe shape, but Condition Block discriminator is metadata.condition.type: "modular_block", value is the block UID. The MB field itself is inline: it needs no resolvedReferences. A reference inside a block does, and its path must carry the block uid: blocks.<block_uid>.<ref_field>. See § Modular Block to reference: the block-uid segment |
| data_type: "file", multiple: true (multi-file) | Multi-reference recipe minus resolvedReferences (files don’t need resolution). Minus Condition Block (one shape). Like group-multiple. Path uses .0.url etc. inside each iteration. |
| data_type: "<any scalar list>" (array of strings, numbers, etc.) | Group-multiple recipe shape. Each iteration is one scalar. The card might be a single component reading the scalar via repeater scope |
The OPEN doc’s P19 captures exactly the group-multiple vs reference split: same iteration shape, different prerequisites (resolvedReferences + Condition Block) depending on the source.