Composition Building Blocks
The shared vocabulary for both the Section Catalog and the Template Catalog. Assumes the Create Compositions guide (entry fields, authentication, the zlib encoding, node anatomy).
How to read it. Each block is defined once here: its node shape plus a table of its attributes. The two catalogs assemble these blocks into real Section and Template cases and point back here rather than re-explaining anything.
Every bindable prop carries a type from a closed set of 13. The Prop value type reference table sits at the end. Skim it first if the { "type": … } wrappers below are unfamiliar.
Blocks are composition-agnostic. The same block JSON drops into a Section, a linked Template, or a freeform Template unchanged. The only thing that varies between the three is which value source feeds the block. See Data-source availability by composition. Read that once and every block below applies everywhere.
Start here: the anatomy of a node
Every entry in the catalog below is a node you drop into a composition's ui tree. Before scanning the catalog, learn the anatomy once so you can read every entry the same way.
See also: Create Compositions via the API, 5. The ui spec (layout tree): the same fields shown as a terse TypeScript type plus the empty starting spec you POST to create a composition. This anatomy section is the walkthrough with worked examples. The index-page section is the type reference.
A node has seven keys. Six carry data. One (attrs) is reserved for future use and is always {}.
{
"uid": "V1StGXR8_Z5jdHi", // 15-char nanoid, unique within THIS composition's tree
"type": "text", // built-in name, OR a registered component's `type` string
"attrs": {}, // reserved; keep as {}
"metadata": { /* … */ }, // extras: bindings, condition, exposed props, mode, etc.
"props": { /* … */ }, // the component's own props (bindable)
"slots": { /* … */ }, // childUID → child nodes (the tree)
"styles": {} // per-group style overrides; {} when unstyled
}Key by key, what to know:
- uid: a 15-char nanoid. Only needs to be unique within this composition's ui tree, not globally. The catalog uses short mnemonic uids like rep1, slot1, ct1 in place of real nanoids so the referring keys (like repeaterUID: "rep1") are visibly matched to their targets.
- type: either a built-in name (see the Built-in node types reference) or the string a component was registered under. type: "text" renders Studio's built-in text block. type: "simple-card" renders <SimpleCard> if you've registered it with registerComponent({ type: "simple-card", … }).
- attrs: reserved. Always {}. Do not put custom data here.
metadata: carries anything that isn't a prop the component accepts. Common contents: node-level bindings on a page root (worked example below), a condition object on a condition-block, mode: "preview" on a repeater, compositionUID on a section-composition, sectionExposedProps on a section's page root.
Node-level metadata.bindings: worked example. Used on the root page node to declare bindings that apply to the page as a whole (or on nodes that need a binding but don't have a corresponding prop). Shape mirrors props.<propName>.binding:
{ "type": "page", "metadata": { "bindings": { "pageTitle": { "type": "template", "value": { "path": { "title": {} } } }, "canonicalUrl": { "type": "template", "value": { "path": { "url": {} } } } } }, "props": { "children": { "type": "slot", "slot": "root" } }, "slots": { "root": [ /* … */ ] } }Common uses on the page root: SEO metadata bindings (title, description, canonical), locale flags, or Section-level sectionExposedProps (Case 14 in the Section Catalog). If a binding has a matching prop on the component, put it in props. If it doesn't (page-scope metadata), put it here.
- props: the shape a component author would recognise. Each prop is a { type, binding } pair: type is one of the 13 prop value types (string, plaintext, imageurl, href, slot, and so on), binding is where the value comes from (see Value sources). A prop whose type is slot uses a different shape. See the next section.
- slots: the child tree. Each key is a slot UID (matched to whatever props.<propName>.slot points at). The value is an array of child nodes rendered inside that slot.
- styles: per-group style overrides. Empty {} when the node uses defaults.
How your React component's props become the JSON props
If <SimpleCard> is a React component that accepts text and buttonLabel as string props today:
function SimpleCard({ text, buttonLabel }) {
return <div>{text} — {buttonLabel}</div>;
}and you register it:
registerComponent({
type: "simple-card",
component: SimpleCard,
props: {
text: { type: "string" },
buttonLabel: { type: "string" },
},
});then a simple-card node in your ui tree encodes each prop as { type, binding }:
{
"type": "simple-card",
"props": {
"text": { "type": "string", "binding": { "type": "template", "value": { "path": { "title": {} } } } },
"buttonLabel": { "type": "string", "binding": { "type": "static_value", "value": "Learn more" } }
}
}Read the mapping: for every registered prop, the props.<propName> object in your ui node carries a type (matching the registered prop's type) and a binding (any of the six value sources). If a prop's value type is slot, the encoding is different: a slot prop uses { type: "slot", slot: "<slotUID>" } and the actual children live in the node's slots["<slotUID>"] array.
The simplest possible node: a "hello world"
Bind the connected entry's title field to a built-in text node:
{
"uid": "V1StGXR8_Z5jdHi",
"type": "text",
"attrs": {},
"metadata": {},
"props": {
"text": {
"type": "plaintext",
"binding": { "type": "template", "value": { "path": { "title": {} } } }
}
},
"slots": {},
"styles": {}
}Everything you see below in the catalog is a variant of this shape. As you scan, look for: what type is it, what does its props map to, and does it have slots with children? That's the whole reading pattern.
Slots: how child nodes attach to a parent
Almost every block below the "Single value" case has children. Learn the slot mechanism now and every one of them will read the same way.
A slot is Studio's answer to "where inside this parent do the children go?" It's a two-part contract expressed in two adjacent fields of the same node:
- A props entry with type: "slot": declares that this component accepts children at this named position, and points at a slot UID.
- A slots entry keyed by that same slot UID: holds the actual array of child nodes.
The slot UID on both sides has to be the exact same string. That's the whole link.
Minimal example: a parent with one text child
Here's a box (a built-in Studio container from the Structure group) that wraps a text node. Watch how the slot string on the parent's props.children.slot matches the key in the parent's slots:
{
"uid": "box_a",
"type": "box",
"attrs": {},
"metadata": {},
"props": {
"children": {
"type": "slot", // ← this prop is a placeholder for child nodes
"slot": "s1" // ← the slot UID it references
}
},
"slots": {
"s1": [ // ← same UID; holds the actual children
{
"uid": "txt_a",
"type": "text",
"attrs": {},
"metadata": {},
"props": {
"text": { "type": "plaintext",
"binding": { "type": "template", "value": { "path": { "title": {} } } } }
},
"slots": {},
"styles": {}
}
]
},
"styles": {}
}Read the two parts side by side:
- props.children is { type: "slot", slot: "s1" }: "there's a slot here called s1."
- slots["s1"] is [ <text node> ]: "and here's the array of nodes that live inside s1."
children is not a magic name. It's just the conventional prop name Studio's built-in containers (like box, page) declare for their child slot. Your own registered components can name their slot prop whatever they want: a <Card> with a header slot and a footer slot might expose headerSlot and footerSlot instead of children.
More than one slot: a component with named regions
A component can declare multiple slot props. Each gets its own slot UID and its own array in slots. For example, imagine a registered two-column component with left and right slot props:
{
"type": "two-column",
"props": {
"left": { "type": "slot", "slot": "sL" },
"right": { "type": "slot", "slot": "sR" }
},
"slots": {
"sL": [ /* children rendered in the left column */ ],
"sR": [ /* children rendered in the right column */ ]
}
}Whatever slot props the component's registration declares, the composition JSON encodes them the same way: one props.<name> entry + one matching slots["<slotUID>"] array per slot.
The one prop type that isn't bindable
Of the 13 prop value types, slot is the only one that cannot carry a binding. Every other prop is { type, binding }. A slot prop is { type: "slot", slot: "<slotUID>" }. That's the difference: a slot prop holds nodes, not a value from a data source.
Slots are how every container block works
Every block in the catalog below that has children uses this exact mechanism:
- A Repeater has a slot for its per-item template.
- A Condition Block has a slot for the branch that renders when the condition matches.
- A Section slot is a specific kind of slot, one a Template can fill from outside.
- A Registered component with slot props uses this whenever a slot prop is declared.
- The root page node itself uses a slot to hold the composition's top-level tree.
Once you can spot props.<name>.slot and its matching entry in slots, you can read every container block in the catalog without re-learning anything.
How to read each catalog entry
Every block below follows the same layout:
- One-line purpose: italicised summary of what the block does.
- The JSON shape: the minimal working spec.
- Attributes table: every key that varies, with what it does.
- Fed by callout (where applicable): which value sources feed this block, and how the answer differs between Linked Template, Freeform Template, and Section (per the availability matrix).
- Cross-references: links to the UI-guide equivalents and to catalog cases that use this block in a full worked example.
You do NOT need to read the catalog top-to-bottom. Skim the block you need, copy the JSON, adjust the binding and (if applicable) inner path. That's the workflow.
Building blocks
Each block is a node you drop into a composition's ui tree, shown once with the attributes it carries. The catalog cases assemble these and point back here rather than re-explaining anything.
About the uids. Two placeholder styles run through every example. Mnemonic node uids (rep1, slot1, ct1, block1) stand in for the 15-char nanoids real specs use (e.g. V1StGXR8_Z5jdHi). They're kept short so a repeaterUID visibly matches the node uid it points at. Elided entry uids (blt8cdca5…, <section entry uid>) substitute for real Contentstack uids. Neither is a UUID.
Single value
Bind one field to a prop.
{
"type": "text",
"props": {
"text": {
"type": "plaintext",
"binding": { "type": "template", "value": { "path": { "title": {} } } }
}
}
}Reading this JSON, key by key:
- "type": "text": the node's type is "text", one of Studio's built-in node types (the "Content" group). It renders Studio's built-in text component. If you wanted to render your own registered component instead, this would be the string you passed to registerComponent({ type: "..." }). See Registered component.
- "props": { "text": … }: the built-in text node accepts one prop named text. If this were your own component with props headline and subhead, you'd see two keys under props instead.
- "text": { "type": "plaintext", "binding": { … } }: every bindable prop is a { type, binding } pair. type here is the prop's value type (one of the 13 in Prop value type, "plaintext" = unformatted text). binding is where the value comes from.
The attribute table below explains the two keys inside binding that vary per case.
| Attribute | What it does |
|---|---|
| binding.type | which source the value comes from (here "template" = the entry the Section / Template is connected to). See Value sources for the six options. |
| binding.value.path | the field to read: { "title": {} } maps to the entry's title. Nest for deeper fields ({ "hero": { "cta": {} } }). An empty {} means "the whole bound field". |
A single group binds the same way: reach its inner field directly, e.g. { "path": { "textbox": {} } } (Section Catalog Case 4).
Fed by: the template source on a linked Template or a Section. In a freeform Template (no connected entry) the same node reads a pinned entry (contentstack) or a static_value instead, see availability.
Constructing binding.value.path: worked examples on a real content type
path is how you say "which field on the entry do I want." It's an object shaped like a slice of your content-type tree: each key is a field name, and its value is either another path object (drill deeper) or an empty {} (stop here, take the whole value at this leaf).
The empty {} matters. It's the leaf marker. Every path ends in {} somewhere. That's how you say "take the value at this key." An empty path: {} at the top level means "the whole bound scope" (used when the enclosing Repeater is iterating the whole scope, see below).
Here's a concrete content type, call it blog_post:
blog_post
├── title (single line)
├── body (rich text)
├── hero_group (Group)
│ ├── headline (single line)
│ ├── subhead (single line)
│ └── cta (Group)
│ ├── label (single line)
│ └── href (URL)
├── body_sections (Modular Block — allowed block-types: text_block, image_block)
│ ├── text_block → { body: rich text, align: choice }
│ └── image_block → { image: file, caption: single line }
└── related (Reference multi — target CTs: article_ref, podcast_ref)Now the path for common cases against blog_post:
| What you want to read | path |
|---|---|
| The entry's title | { "title": {} } |
| The rich-text body | { "body": {} } |
| A field one level inside a Group (hero_group.headline) | { "hero_group": { "headline": {} } } |
| A field two levels deep (hero_group.cta.label) | { "hero_group": { "cta": { "label": {} } } } |
| The whole hero_group group as one blob | { "hero_group": {} } |
| The whole hero_group.cta subgroup | { "hero_group": { "cta": {} } } |
| The body_sections Modular Block array (feed to a Repeater's items) | { "body_sections": {} } |
| The related Reference-multi array (feed to a Repeater's items) | { "related": {} } |
Inside a Repeater: the binding shifts to "type": "repeater" and path is relative to the current item, not the entry:
| What you want to read (item context) | path |
|---|---|
| body_sections[i].text_block.body: the text-block's rich-text body | { "text_block": { "body": {} } } |
| body_sections[i].image_block.caption: the image-block's caption | { "image_block": { "caption": {} } } |
| Any modular-block item's block-type key (used by Condition Blocks to discriminate) | { "text_block": {} } (returns non-null only for text_block items) |
| Any reference item's target content-type uid (used by CBs) | { "_content_type_uid": {} } |
| A field on the referenced entry itself once a CB has matched, e.g. article_ref.title | { "title": {} } (the CB has narrowed the context to the target entry) |
When path is {}: completely empty. This means "the whole bound scope, unnamed." It shows up when:
- A Section is bound to a specific field (via linked_schemas[].selected_field: "body_sections"), and a Repeater inside the Section iterates the scope itself. The Repeater's items path is {} because the whole scope IS the array.
- A prop needs the whole entry as an object.
How to build your own path (the mental checklist):
- Look at your content type's field tree.
- Trace from the root down to the field you want, listing each intermediate group / block / field name in order.
- Write those names as nested keys, each with {} as its value.
- The last (deepest) key still has {}. That's the "take this value" marker.
- If you're inside a Repeater, start counting from the current item's shape, not the entry root.
If the field is a Group with multiple: true, a Modular Block, or a Reference-multi, you don't drill into individual items via path: you feed the whole array to a Repeater's items and let the Repeater walk it. Children of the Repeater then use type: "repeater" bindings with path relative to each item, per the table above.
Value sources
The binding.type says where a value comes from. There are six types:
{ "binding": { "type": "template", "value": { "path": { "title": {} } } } } // a field on the connected entry
{ "binding": { "type": "static_value", "value": "Featured" } } // value is a KEY into the entry's static_value group (see note), not the literal
{ "binding": { "type": "contentstack",
"value": { "uid": "blt…", "_content_type_uid": "author", "path": { "title": {} } } } } // a field on one pinned entry
{ "binding": { "type": "contentstack_queries",
"value": { "queryUID": "query-1", "path": { "entries": {} } } } } // the results of a query (an array)
{ "binding": { "type": "repeater",
"value": { "repeaterUID": "rep1", "path": { "title": {} } } } } // a field on the current repeater item
{ "binding": { "type": "component_props", "value": "title" } } // a prop the host app passes to the component| Binding type | Reads from | Needs a data_sources entry? |
|---|---|---|
| template | a field on the connected/linked entry, the scope of a linked template or a section, not available in a freeform template (no connected entry) | no |
| static_value | the composition's static_value group, looked up by binding.value as a key (not the literal), see note | no |
| contentstack | a field on one specific pinned entry | yes (contentstack) |
| contentstack_queries | the results of a saved query (an array) | yes (contentstack_queries) |
| repeater | the current item while inside a Repeater | no |
| component_props | a prop the host app passes into the component (its default data) | no |
static_value takes a KEY, not the literal. binding.value names an entry in the composition's static_value group. The SDK resolves it as static_value.<value>. The key follows the editor's <nodeUID>-<propName> convention (e.g. "9c98…-text"). The actual value lives in the entry's static_value field, a group organized by field type (text, choice, html_rte, and so on), each a list of { key, value }, which the SDK flattens to a key → value map. It renders only if that group carries a key matching binding.value. The static_value field starts empty at creation (see the entry-fields table in index.md). If the key is absent, the component's own default/placeholder renders, not the string in binding.value.
Declaring the two that need it. A contentstack (pinned entry) and a contentstack_queries (query) binding only resolve if the entry or query is declared in the composition's data_sources:
"data_sources": [
{
"uid": "contentstack",
"data": [{ "uid": "blt…", "_content_type_uid": "author" }]
},
{
"uid": "contentstack_queries",
"data": [
{
"uid": "query-1",
"name": "Authors",
"spec": {
"contentType": "author",
"sourceType": "content_type",
"query": {},
"params": { "limit": 10, "skip": 0, "desc": "created_at" }
}
}
]
}
]The query spec has four fields: contentType (the content-type or taxonomy uid), sourceType ("content_type" (the default) or "taxonomy"), query (a filter object), and params (limit, skip, asc/desc, include, only). query and params are free-form.
More detail (prose/UI guides): for binding on the canvas, see CMS Binding and The Data tab. For pinned entries, see Pinned Entries. For queries, see Pinned Queries and Query Builder. For component defaults, see Component Default Data.
Data-source availability by composition
The six sources above are available in all three composition flavors, with one exception and two section-only wrinkles. The template source is the only one whose availability varies. Everything else differs only in emphasis.
| Source | Linked Template | Freeform Template | Section |
|---|---|---|---|
| template | the connected entry: a single content type, resolved from the request URL | No: there is no connected entry | the linked-schema scope: can span multiple content types (linked_schemas is an array), narrowed by selected_field, and repointable via a Binding override when embedded |
| contentstack (pinned entry) | Yes, its own | Yes, its own | Yes: declared on the section and travels with it into whatever template embeds it |
| contentstack_queries (query) | Yes, its own | Yes, its own | Yes: travels with the section too |
| static_value | Yes | Yes | Yes |
| repeater | Yes | Yes | Yes |
| component_props | Yes | Yes (a common freeform choice, component default data) | Yes |
The one rule that matters: a freeform Template has no template source, so every entry-shaped block below (single value, repeater, condition, groups, reference, modular blocks) is fed by a pinned entry, a query, a static value, or component default data instead. The block's shape is identical: only the binding type changes.
Repeater
Iterate a collection, rendering its children once per item.
{
"uid": "rep1",
"type": "repeater",
"metadata": { "mode": "preview" },
"props": {
"items": {
"type": "array",
"binding": { "type": "template", "value": { "path": {} } }
},
"children": { "type": "slot", "slot": "items" }
},
"slots": {
"items": [
{
"type": "text",
"props": {
"text": {
"type": "plaintext",
"binding": {
"type": "repeater",
"value": { "repeaterUID": "rep1", "path": { "title": {} } }
}
}
}
}
]
}
}| Attribute | What it does |
|---|---|
| props.items.binding | the collection to walk. The path you write depends on where the scope starts. See "What path should I use here?" below the table. |
| metadata.mode | "preview" or "design". "preview" renders every item (what a live page shows). "design" renders a single editable placeholder. If you omit it, Studio derives the mode: "design" when the section exposes a section-slot, otherwise "preview". |
| metadata.repeaterBindingFieldType | "reference" or "modular_block": tells Studio the field kind so a Condition block can match. Absent for plain groups. |
| slots.<uid> | the per-item template. Children read the current item with a repeater binding whose repeaterUID equals this repeater node's uid. |
A Repeater is the normal way to iterate a collection inside any composition's own ui, section or template. It's independent of a Section slot: a slot can inherit an enclosing Repeater's scope (see below), but a Repeater never needs a slot to work.
<a id="repeater-items-path"></a>
What path should I use for items.binding.value.path?
Every composition has a scope: the thing bindings resolve against. That scope is different for Sections and Templates, which is why the same Repeater takes different path values in each:
- On a Section, the scope is whatever linked_schemas[].selected_field names. If the Section's selected_field is "body_sections", the scope IS entry.body_sections (already the array. So the Repeater's path is {} (empty)) meaning "iterate the scope directly, no drilling needed."
- On a Template, the scope is the entire entry (via connected_content_type). To iterate a specific field like entry.body_sections, you have to name it: path: { "body_sections": {} }.
Same content type, same iteration, two different path values, because the scope you start from is different.
Worked examples using the blog_post content type shown earlier:
| Composition | Scope | You want to iterate | path on items |
|---|---|---|---|
| Section with selected_field: "body_sections" | entry.body_sections (already the array) | body_sections | {} |
| Section with selected_field: "related" | entry.related (already the array) | related | {} |
| Section bound to the whole entry (selected_field omitted) | entry | body_sections | { "body_sections": {} } |
| Section bound to the whole entry | entry | related | { "related": {} } |
| Linked Template (any) | the connected entry | body_sections | { "body_sections": {} } |
| Linked Template (any) | the connected entry | related | { "related": {} } |
| Freeform Template (no scope) | - | a saved query's entries | { "entries": {} } with binding.type: "contentstack_queries" |
Rule of thumb: if your Section's selected_field is literally the array you want to iterate, use path: {}. If you're one or more levels above it, name each hop until you reach the array.
The path table in the earlier "Constructing binding.value.path" section applies here too: this is the same path shape, fed to a Repeater's items instead of a scalar prop.
Reading the current item (the repeater context). The text child in the slot above reads each item's title with the repeater value source: binding.value.repeaterUID must equal this Repeater node's uid, and path is the field to read off the current item. Every binding inside the slot (including nested Repeaters and Condition blocks) resolves against that item. That inherited scope is the repeater context. See Groups / Reference / Modular blocks for more worked children.
More detail (prose/UI guide): Smart Containers: Repeaters: Preview Mode, selecting a Repeater from Layers, and when to use one.
Fed by: template (a collection field on the entry / linked-schema scope) or contentstack_queries (a query). In a freeform Template the items must bind a query or a pinned reference. There is no template collection to walk. See availability.
Condition block
Render a branch only when the current item matches a discriminator.
{
"type": "condition-block",
"metadata": {
"condition": {
"type": "reference",
"operator": "eq",
"value": "ct2",
"conditionBinding": {
"type": "repeater",
"value": { "repeaterUID": "rep1", "path": { "_content_type_uid": {} } }
}
}
},
"props": {
"children": { "type": "slot", "slot": "body" }
},
"slots": {
"body": [ /* rendered only for the matching item */ ]
}
}| Attribute | What it does |
|---|---|
| condition.type | which kind of field the branch tests. See the type list below. |
| condition.value | the value this branch matches: a content-type uid for reference, a block uid for modular_block, and so on. |
| condition.operator | how value is compared. The valid operators depend on condition.type (see the operator table). |
| condition.conditionBinding | a repeater-scoped binding that reads the tested field off the current item: _content_type_uid for a reference, the block key for a modular block. It may also carry content_type_uid + selected_field to qualify the source field (always for modular blocks, optionally for references). These two are authoring metadata, not read by the runtime evaluator (which uses repeaterUID + path). |
| condition.dataBinding | points at the matched item's data so children can bind into it. Present on every condition type (most used with modular_block). The runtime evaluator itself reads only conditionBinding. |
| slots.<uid> | rendered only when the current item matches. |
One condition per block. A condition block holds a single condition. There is no and/or combinator and no negate. To require several conditions at once, nest condition-blocks (each level is an implicit AND).
Condition types. In practice you'll almost always use reference and modular_block: those are what the composition editor writes. The runtime supports the full set, so a spec authored through the API can use any:
| condition.type | Tests a | value is |
|---|---|---|
| reference | reference field | a content-type uid |
| modular_block | modular-blocks field | a block uid |
| string | text field | a string |
| number | number field | a number |
| boolean | boolean field | true / false |
| array | any array, by length | a number |
| date | date field | an ISO date string |
| select | select field | the chosen option |
| image_extension | file field's extension | an extension (e.g. "png") |
| image_size | file field's size | a number of bytes |
Operators, by type. Which operator values are valid depends on condition.type:
| Type(s) | Valid operators |
|---|---|
| reference | contentTypeIs, eq (plus the array-length operators, to test how many references) |
| modular_block | is, eq (plus the array-length operators) |
| string | eq, neq, contains, startsWith, endsWith, matches, isEmpty, isNotEmpty |
| number, date, image_size | eq, neq, gt, gte, lt, lte |
| boolean | is, eq |
| select (single) | is, eq, isNot |
| select (multiple) | includes, excludes |
| array (length) | lengthIs, lengthIsNot, lengthIsGreaterThan, lengthIsLessThan, lengthIsGreaterThanOrEqualTo, lengthIsLessThanOrEqualTo |
| image_extension | is, eq, isNot |
Every reference/modular-block example in this catalog uses operator: "eq", the common case.
More detail (prose/UI guide): Smart Containers: Condition Blocks.
Groups and global fields
A group is a bag of fields on the entry. A global field behaves identically.
Single: bind the inner field directly (Single value). Multiple: iterate with a Repeater, reading each row's inner field via a repeater binding:
{
"uid": "rep1",
"type": "repeater",
"props": {
"items": {
"type": "array",
"binding": { "type": "template", "value": { "path": {} } }
},
"children": { "type": "slot", "slot": "r" }
},
"slots": {
"r": [
{
"type": "text",
"props": {
"text": {
"type": "plaintext",
"binding": {
"type": "repeater",
"value": { "repeaterUID": "rep1", "path": { "single_line": {} } }
}
}
}
}
]
}
}- A section declares its scope with linked_schemas[].selected_field. A template scopes via its connected_content_type + the entry the URL resolves. See the availability matrix above and each catalog's preamble.
- A dotted selected_field ("group1.inner_group") targets a group nested inside another group: the section is authored against the inner field and a template drives it (Section Catalog Case 12).
- A global field with multiple: true uses this exact shape.
Reference
A reference field points at entries of one or more content types. Iterate + branch per type.
Composed from Repeater + Condition block:
{
"uid": "rep1",
"type": "repeater",
"metadata": { "repeaterBindingFieldType": "reference" },
"props": {
"items": {
"type": "array",
"binding": { "type": "template", "value": { "path": {} } }
},
"children": { "type": "slot", "slot": "r" }
},
"slots": {
"r": [
{
"type": "condition-block",
"metadata": {
"condition": {
"type": "reference",
"operator": "eq",
"value": "ct2",
"conditionBinding": {
"type": "repeater",
"value": { "repeaterUID": "rep1", "path": { "_content_type_uid": {} } }
}
}
},
"slots": {
"c": [ /* the referenced entry's fields, read via `repeater` */ ]
}
}
]
}
}Add one Condition block per referenced content type. Children read the referenced entry via a repeater binding on the repeater's uid.
More detail (prose/UI guide): Smart Containers: References.
Modular blocks
A modular-blocks field holds items of different block types.
The same shape as Reference, but the Condition block switches on a block type and carries a dataBinding alongside conditionBinding:
{
"uid": "rep1",
"type": "repeater",
"metadata": { "repeaterBindingFieldType": "modular_block" },
"props": {
"items": {
"type": "array",
"binding": { "type": "template", "value": { "path": {} } }
},
"children": { "type": "slot", "slot": "r" }
},
"slots": {
"r": [
{
"type": "condition-block",
"metadata": {
"condition": {
"type": "modular_block",
"operator": "eq",
"value": "block1",
"conditionBinding": {
"type": "repeater",
"value": {
"repeaterUID": "rep1",
"content_type_uid": "price_range",
"selected_field": "modular_blocks",
"path": { "block1": {} }
}
},
"dataBinding": {
"type": "repeater",
"value": { "repeaterUID": "rep1", "path": { "block1": {} } }
}
}
},
"slots": {
"c": [ /* the block's fields, read via `repeater` */ ]
}
}
]
}
}One Condition block per block type.
More detail (prose/UI guide): Smart Containers: Modular Blocks.
Registered component
Render a component the consuming app registered with the SDK. The node type is the registered name. props are the component's own props, bindable via any value source.
{
"type": "simple-card",
"props": {
"text": {
"type": "string",
"binding": { "type": "template", "value": { "path": { "title": {} } } }
},
"buttonLabel": {
"type": "string",
"binding": { "type": "static_value", "value": "Learn more" }
}
}
}The built-in nodes (text, box, header) are Studio's own registered components, a custom component behaves the same way, only the type and prop names change. In a freeform Template, a registered component's props are a natural fit for component_props (the component's own default data).
More detail (prose/UI guides): Bring Your Own Components, Registering components, Component schema: prop types.
Mechanics: cross-cutting, used by the blocks above.
Section slot
An empty placeholder the template fills.
{
"type": "section-slot",
"props": {
"label": {
"type": "string",
"binding": { "type": "static_value", "value": "Drop a card here" }
}
}
}| Attribute | What it does |
|---|---|
| props.label | the placeholder text a template author sees on the canvas (a static_value). |
| (no slots of its own) | the slot is filled on the template side: the embedding section-composition node keys its slots on this node's uid. See the catalog's slot cases. |
| scope | content a template drops into the slot inherits the surrounding repeater context: it sees the current iteration item of whichever Repeater encloses the slot. That can be the section's own Repeater (the slot sits inside it), or the page's Repeater when the template drops the whole section inside its own Repeater, the page's repeater context flows into the slot. |
Defined in a Section. Filled by either Template flavor. A section-slot node can only be added while authoring a Section (it isn't in a Template's palette). But any Template, linked or freeform, can fill one (freeform templates embed sections the same way linked ones do).
More detail (prose/UI guide): Smart Containers: Section Slots.
Binding override
When a template places a section, repoint the section's scope with sectionBindingOverride on the section-composition node.
{
"type": "section-composition",
"metadata": {
"compositionUID": "<section entry uid>",
"sectionBindingOverride": {
"selectedField": "price_range_modular_blocks",
"uidRemapping": {}
}
}
}| Attribute | What it does |
|---|---|
| selectedField | the field the placed section binds against, relative to where the embed sits: a sibling field of the page CT (Case 6), or an inner leaf when the embed sits inside an outer Repeater (Cases 12/13). |
| uidRemapping | { <section-side key>: <page-side key> }: relabels the page's data so the section's bindings resolve. Identity ({"single_line":"single_line"}) or empty {} when both sides use the same field/block uid. |
Two uses: repoint to a sibling field (Case 6, the simple form), or narrow to an inner field inside an outer repeater (Cases 12/13).
Applied by either Template flavor. sectionBindingOverride isn't gated on a connected content type. A freeform Template can repoint an embedded section too (e.g. to a field on a pinned entry or a query item it iterates).
More detail (prose/UI guides): Using sections and components in a template, Per-page overrides without forking.
Exposed section prop
A section author marks one internal component prop as overridable per template instance. Stored as sectionExposedProps on the section root's ui.metadata, not a node you drop. The section-author counterpart to a Binding override: the section lists what's negotiable, the template overrides it. overrides it.
{
"ui": {
"metadata": {
"sectionExposedProps": [
{
"nodeUid": "<uid of the internal component instance>",
"propKey": "text",
"uid": "card_title",
"displayName": "Card Title",
"propType": "string",
"bindingAtExposeTime": {
"type": "template",
"value": { "path": { "title": {} } }
}
}
]
}
}
}| Attribute | What it does |
|---|---|
| nodeUid | the internal component instance whose prop is exposed. Exposure is per instance: three Heroes each expose their own headline independently. |
| propKey | the original schema key on that component ("text", "headline", "src"). The Component Prop column in the Expose modal. |
| uid | slug generated from displayName ("Card Title" becomes card_title). The key the page node carries the override under. The runtime maps it back to nodeUid/propKey. Must be unique within the section. |
| displayName | the Exposed As label the template author sees in the override panel. |
| propType | the prop's value type (string, href, imageurl, and so on). Decides the override control: string gives a text input, imageurl an image picker, href a link picker. |
| bindingAtExposeTime | snapshot of the internal node's binding when it was exposed. Lets the runtime determine whether a page override applies or the prop falls through to the section's own binding. Omit for an unbound prop. |
At render the runtime spreads the page composer's values onto the section and builds section_exposed_overrides = { [nodeUid]: { [propKey]: value } }: the sole source of truth for each exposed field (an entry is emitted even for a cleared value, so it never falls back to the internal binding). Prose walkthrough: Expose Section Props.
Value reference
Two closed sets that the blocks and cases rely on.
Prop value type
Every bindable prop carries a type (the { "type": …, "binding": … } wrapper). There are 13:
| type | Use for |
|---|---|
| string | short or long text |
| plaintext | text with no formatting (the current text-node type) |
| boolean | a true/false toggle |
| number | a number |
| datestring | a date or date-time |
| href | a link / URL |
| imageurl | an image or asset URL |
| object | a group of nested sub-props |
| array | an editable list |
| slot | a drop target holding child nodes (the only type that can't be bound) |
| choice | a single- or multi-select option |
| json_rte | a JSON rich-text document |
| any | an arbitrary value |
A few near-misses are not their own type: rich text maps to json_rte, a file or asset to imageurl, a link to href, a group to object, a select to choice.
More detail (prose/UI guides): for prop types, see Component schema. For rich text, see JSON RTE.
Built-in node types
A node's type is either one of Studio's built-ins below, or the name of a component the host app registered. The built-ins:
| Group | Types |
|---|---|
| Structure | page (the root), section, box, hstack, vstack, fragment |
| Smart containers | repeater, condition-block, section-slot, section-composition |
| Content | text, plain-text, rich-text, json-rte, collapsible-text, header, number, button, link, link-container |
| Media | image, video, embed |
| Raw HTML | html-element, style-sheet |
Any type that isn't in this list is a registered component: its type is the name it was registered under, and its props are that component's own props (see Registered component).
Quick-reference cheat sheet
One-liners for every block and mechanic, in the order they appear above. Use this after you've read the anatomy section: it's the reminder card you keep open while writing JSON.
| Need | Use | One-liner |
|---|---|---|
| Bind one field to a component prop | Single value | { "type": <propType>, "binding": { "type": "template", "value": { "path": { "<field>": {} } } } } |
| Iterate an array of items | Repeater | Node with type: "repeater", items bound to the collection, children in a slot that read the current item via "type": "repeater" binding with repeaterUID |
| Render different children per item type | Condition block inside a Repeater | Adds a metadata.condition with type, operator, value, and a conditionBinding that reads the discriminator field |
| Bind a single group's inner field | Groups | Same as Single value, with a deeper path (e.g. { "hero_group": { "headline": {} } }) |
| Iterate a group with multiple: true | Groups with a Repeater | Repeater whose items.path = {} (scope root) or the group field name |
| Iterate references to other CTs, render per target CT | Reference | Repeater with repeaterBindingFieldType: "reference" + one Condition Block per target CT (condition.type: "reference", value: "<ct_uid>") |
| Iterate a Modular Block, render per block-type | Modular blocks | Same as Reference but repeaterBindingFieldType: "modular_block" + CBs with condition.type: "modular_block", value: "<block_uid>", plus a dataBinding |
| Render a component you registered | Registered component | Node whose type equals the string you passed to registerComponent({ type: … }). props are { type, binding } pairs matching the registered prop schema |
| Leave a placeholder in a Section for a Template to fill | Section slot | type: "section-slot" node. The embedding section-composition on the Template keys its slots on this node's uid |
| Repoint a Section's scope when a Template embeds it | Binding override | sectionBindingOverride on the section-composition node with selectedField + uidRemapping |
| Let a Template override one internal component prop of a Section | Exposed section prop | sectionExposedProps array on the Section's root page node's metadata. Template writes overrides on the placed section-composition |
| Read a pinned entry | Value sources | { "type": "contentstack", "value": { "uid": "<entry uid>", "_content_type_uid": "<ct>", "path": { … } } } + declare in data_sources |
| Read a query's result list | Value sources | { "type": "contentstack_queries", "value": { "queryUID": "<key>", "path": { "entries": {} } } } + declare the query in data_sources |
| Read a static value stored on the composition | Value sources | { "type": "static_value", "value": "<key in static_value group>" } |
| Read a prop the host app passes in (component defaults, freeform) | Value sources | { "type": "component_props", "value": "<propName>" } |
Prop value types (13): string, plaintext, boolean, number, datestring, href, imageurl, object, array, slot, choice, json_rte, any. See Prop value type for full descriptions.
Built-in node types (5 groups): Structure, Smart containers, Content, Media, Raw HTML. See Built-in node types for the full list.