Studio Docs

When to Use

Verify a composition actually resolves (a 200 does not mean it does), and the full pitfall table for API-authored compositions.

Part of the API-authoring set. Start at author-composition-via-api, which routes to this one.

Verify the write: a 200 does not mean the composition resolves

Nothing validates that a static_value key on a node exists in the entry’s bucket. Write half the pair and the CMA still returns 200. The canvas then renders every affected prop as its registration defaultValue, which looks like a working section made of placeholder copy.

Worked failure. A hero was authored with all seven props stripped to static_value. The write returned 200. The canvas showed “Your headline here” and two buttons both reading “Get started”, indistinguishable from a deliberate skeleton. The entry actually held:

"static_value": {
  "boolean": [ { "value": false, "_metadata": { … } } ],   // ⛔ no "key"
  "href":    [ { "value": "#",   "_metadata": { … } } ],   // ⛔ no "key"
  "choice":  [], "imageurl": [], "number": [], "object": []
}                                                          // ⛔ no "text" bucket at all

Every item was missing its key, and the bucket holding the headline and button labels was never written. Zero of the bindings could resolve, and nothing anywhere reported it.

Run this check after every write. It is three lines and catches the entire class:

  1. Re-fetch the entry and decompress ui.
  2. Walk the tree collecting every (props.<name>.type, binding.value) pair where binding.type === "static_value".
  3. Assert each one has a matching {key} in static_value[<bucket-for-that-type>]. Any miss is a prop that will silently render its default.

Do the same before writing, against the payload you are about to send. On the corrected hero the counts read refs=26 statics=26 missing=[]. The original would have reported 7 missing keys and 1 absent bucket.

The same discipline applies to template bindings: decompose-design § Step 8b is this check at plan time, and build-section § Field-existence gate is it at bind time. A prop that silently falls back to defaultValue is the single failure mode all three exist to catch.

Preflight: assert the envelope on every node before you write

The rule above is stated. Nothing enforces it. A sweep of one live stack found 54 nodes across 10 compositions missing slots (the whole pricing family plus two carousels), every one authored by tooling that skipped it on leaves. They rendered fine for months and only surfaced as a white screen when someone opened the Data tab.

So check the payload you are about to send, and the entry after you write it:

for every node in the ui tree:
  assert all of ("uid","type","props","metadata","slots","attrs","styles") are present and non-null

Three lines, catches the entire class. Run it in the same pass as the static_value key check, same idea, different envelope. On a correct composition it reports zero. On the pricing sections above it would have reported 4, 8, 11.

Repairing existing data is additive: insert "slots": {} where the key is absent, leave everything else untouched. Note it creates an unpublished version on a published composition, so republish afterwards or the live entry keeps the old shape.

Common Pitfalls

PitfallSymptomFix
Rebuilding a template’s root slot from a fresh node listSibling nodes that sat beside the main one (a closing CTA band, a comparison table, a content list) are dropped. The page still renders, so nothing flags itRebuild from the newest version whose root slot holds no section-composition node, and carry every sibling across as its own Section
Deriving a composition title by stripping a suffix from a scope nameTwo scopes collapse to one title. Titles are unique per project, so the second create returns 409 and that Section is missing from the templateKeep the title 1:1 with the scope it is rooted at
Creating a Section and placing it in the same pass without publishingThe Section resolves to nothing at delivery and the page renders Component with type '<uid>' is not registeredPublish every Section before the Template that references it. See complete-the-build
Missing data_sources.resolvedReferencesRepeater renders 0 items, reference stubs never materialiseAdd the resolution map entry: { uid:"template", resolvedReferences: { "root.<refField>": ["<refField>"] } }
Authoring from the registration without reading the component sourceThe registration lists prop names and options but not how they are consumed. A prop that overrides another, or is compared by equality, discards correct values with no errorOpen the component file for every component you author. See § Read the component source before authoring
A list-wrapped choice hitting a strict equality check in component code['auto'] !== 'auto' is true, so an override branch fires and the props it guards are dropped, justify silently stops workingLeave equality-compared props unwritten so the component’s string default applies. Report the array-handling bug to the library owner
Composing atomically without reading how the composite derives its valuesA composite computes child values from its own scheme: isDark ? 'medium' : 'light'. Hand-authoring the same tree with the atom defaults gets colors wrong in a way that looks plausibleTreat the composite’s derivation logic as the specification. See § Read the component source before authoring point 3
Authoring the prop the registration advertises rather than the one the component readsA media atom exposing both url and media uses only media?.url. A url-only node renders nothing at all and reads as a missing design elementRead the component’s first lines: early returns name the prop it actually requires
Writing an object or array value as a real structureThose buckets store a JSON string. A real object fails the whole entry with 422 static_value.object.N.value is not textjson.dumps the value before writing
Relying on height: 100% inside a compositionThe SDK wraps every node in its own unstyled <div>. That wrapper collapses to content height, so 100% resolves against the wrapper, not the card, measured 324px inside a 472px cardUse an explicit px height on the container that must distribute. It does not depend on the wrapper
Assuming a card primitive’s own justify-between will distribute your nodesThe padded inner element of a card component is often justify: normal, and it is inside the component where no prop reaches it. Children pack at the top however they are nestedPut one stack inside with an explicit height (card height minus the component’s own padding) and set justify on that
Setting justify: between on a container with no heightDistribution needs free space. A card whose height was removed to avoid clipping packs its children at the top, so CTAs sit at ragged heights instead of pinned to the bottomGive the container the comp’s height. Note that height: 100% may not chain through the SDK’s per-node wrapper divs
Setting justify on a row that has no widthjustify distributes free space. A shrink-wrapped row has none, so the value applies and nothing moves. A parent stack with align: start shrink-wraps its childrenGive the row a width and the parent align: stretch. Measure the rendered box, not the stored prop
A numeric value written into the number bucket as a stringThe bucket is strictly typed: the CMA rejects the whole entry with 422 static_value.number.N.value is not number. Easy to hit when defaults are parsed out of source, where 1 arrives as "1"Coerce to a real int/float before writing. Only choice wraps in a list. number stays a bare number
Using a decorative shape primitive as an in-flow rule or dividerIts wrapper is position:absolute; inset:0, so it takes no space and pins to the section, not the card. It measures correct and renders in the wrong place: four stacked indicators read as one stray white line over the headingRead the component first. Verify the rendered box sits inside its intended parent, not just that its size and color match
Reporting a value as impossible without checking for a free-form primitiveA Deviations row that says impossible stops anyone looking again, so an expressible value stays unbuilt indefinitelySearch the registry for shape/box primitives with raw CSS color / width / height first
choice value stored as a bare string instead of a listRenders correctly: the canvas is right and every visual check passes, but the Settings panel matches it against the registered options, finds nothing, and shows Select…. The author re-picks a value that was already set"value": ["h1"]. Only choice wraps. Boolean/text/number stay scalar. See § Node anatomy point 0a
Writing only the props the design dictatesEvery other registered prop shows blank in Studio’s Settings panel, so an author has to pick each dropdown by hand: the API author’s omission becomes someone else’s manual workWrite every registered prop: comp value, else the registration’s own defaultValue. See § Node anatomy point 0b
Nodes with no metadata.titleThe Layers tree shows repeated type names (Stack / Box ×6) and cannot be navigatedGive each node a purpose-name: Stack — Card content, CTA — Read the full story
Leaf node authored without metadata / slotsWhole canvas shows Component Loading Error — Cannot read properties of undefined (reading 'visible'). Nothing paints and the message names no nodeEvery node carries the full envelope, "metadata": {} and "slots": {} included. See § Node anatomy point 0
static_value items written without their key (or the bucket omitted entirely)CMA returns 200. Every affected prop renders its registration defaultValue, so the section looks like an intentional placeholder skeletonWrite {key, value} pairs into the bucket matching each prop’s declared type, then read back and resolve every key. See § Verify the write
Design props left unwritten on an API-authored sectionSection renders with the library’s defaults, not the comp: a stack defaults to column, so side-by-side CTAs stack. No errorAuthor design props explicitly, skeleton or not. See § Skeleton first, bind later
Card prop bindings use template type at <refField>.0.<field> (fixed .0. index)EVERY iteration renders item 0, all cards identical (the worst kind: looks “correct” with a single-item list, fails as soon as the list grows)Use repeater type with repeaterUID, path relative to the iteration item, no index
Repeater node missing metadata.mode: "preview"Canvas shows 1 placeholder iteration. Production still renders NSet metadata.mode: "preview" on the Repeater node
Missing Condition Block child of Repeater (reference / modular-block iteration)Iteration silently fails to resolve child bindingsInsert a Condition Block per allowed type (CT for refs, block UID for modular blocks)
Writing url on a template composition but omitting url_metadata (“minimal envelope”)Nothing fails. The CMA returns 201, the live route returns 200, delivery resolves off the url pattern alone, so the omission survives review. But Studio has no derivation to read, treats the composition as legacy_url, and the editor’s Edit-URL panel shows the pattern as an opaque hand-typed string instead of “derived from the connected CT”. The CT link is lost the first time an author edits the URL, and legacy_url matching (entry url field, leading slash, every entry) is stricter than what the pattern was written forAlways write the pair. For Connected on a page-type CT, use url_source: "content_type_url_pattern". For a Freeform identity URL, use default_url_pattern. For a Freeform hand-chosen path, use user_specified_pattern. url_queries is a JSON string. See § url + url_metadata: a matched pair
Compositions CT has no url_metadata group (or is missing the url_source / url_queries sub-fields)CMA silently drops the object: 201 returned, entry reads back with no url_metadata, same legacy_url outcome as omitting it, with no error to traceVerify the CT models url_metadata as a group with both text sub-fields (see provision-studio-project). Read the entry back after the first write and assert url_metadata.url_source is what you sent
Setting url_source: "user_specified_pattern" on a Connected template via the APIReverts: url_queries is only generated by the UI’s Edit-URL then Save flow, and Studio re-derives from the CT pattern on editor loadMake the CT a page type (is_page: true + CT url_pattern), then use content_type_url_pattern with {{entry.x}} in the composition url. user_specified_pattern on the API path is for Freeform only
API-authored Section with no ui_previewBlank/placeholder tile in Studio’s Sections accordion for every section, while UI-authored sections show a picture. Silent: the editor screenshots + uploads + PUTs ui_preview on save, and that chain only exists in the UIRe-save each section in Studio (backfills automatically), or upload an asset + PUT {"entry":{"ui_preview":"<assetUid>"}} + publish both asset and entry. See § Section thumbnails
ui_preview set to an unpublished asset uidSame blank tile: the accordion resolves ui_preview.url against the delivery environment and gets nothing. Looks identical to never setting the fieldPublish the asset to the environment the Studio project targets, not just the entry
composable_uid not equal to the CMA entry uid“Composition Not Found”: canvas link uses entry uid, resolution uses composable_uidSet composable_uid = the entry uid before publishing
Authored composition is unpublishedNot visible in compositions list. Can’t load in canvasPublish the entry. Studio queries by composable_uid AND published state
linked_schemas modeled as a reference field on the compositions CT instead of group-multipleSection’s selectedField derived from linked_schemas is always empty, so section scope is unset and repeaters render blank in the editor (P23). Runtime may still work via sectionBindingOverride.Ensure compositions CT models linked_schemas as a group-multiple, see provision-studio-project
Treating this skill as a UI-authoring guideWastes time. Introduces shape inconsistenciesUse only for headless / scripted / seed / migration work. For interactive authoring, the Data Picker writes every shape correctly.
Cross-stack migration: carrying over the source composable_uid instead of re-deriving to the destination entry uidSSR resolves (matches by composable_uid) but the canvas + client render fail (per section) because they key by entry uid. Looks like it works until you open the editorRe-derive each section’s composable_uid to its new destination uid. Rewrite the template’s ui compositionUIDs + linked_sections to the destination uids. See § Migrating compositions between stacks
Cross-stack migration: copying binding index paths / selected_field verbatimDestination CT may order blocks differently or nest a field elsewhere (image vs image_options.image), which produces the wrong block or a blank field, with no errorDiff source vs destination CT schema + entry block order. Remap indices and field paths before writing
Binding into a single reference field’s NESTED asset (e.g. hero.0.image_options.image.url) without listing the reference in data_sources.resolvedReferencesScalars on the referenced entry may resolve (heading/body render) but the nested file/asset does not, so the binding returns undefined and the image is blank, with no errorAdd the reference path to resolvedReferences.template (e.g. "hero", and the nested "hero.image_options.image") so the SDK deep-resolves it. Then bind …image_options.image.url. The file field resolves to an object with .url, matching the production component. One instance of the general rule: § Where a reference path must terminate
Linking a DAM / Assets-Manager asset (am… uid, parent_uid) to a file field via OAuth Bearer CMA writeRejected with 422 "is not a valid upload.": the Bearer app-token grant can’t link DAM assets, even though the asset exists + is publishedUse a session authtoken for the write (it succeeds), or link via the DAM/asset picker in the UI. Note: DAM assets also emit a /spaces/… preview delivery URL that 404s in Live Preview while the published CDA emits images.contentstack.io/…, see troubleshoot-canvas
Placing bindings directly on the node instead of props.<propName>.bindingThe SDK can’t find the binding, so the prop renders with defaultValue or emptyAlways wrap: "props": { "<propName>": { "binding": { type:…, value:{…} } } }.
Using children: [...] instead of slots: { <slot-uid>: [...] } for child nodesChildren invisible to the renderer. Canvas renders the parent as emptyUse slots. Each slot has a uid. See node anatomy above.
Placing Condition Block’s discriminator on the node as condition: {...} instead of metadata: { condition: {...} }Discriminator ignored. Iteration item’s type can’t be narrowed. Bindings inside fail to resolvePut condition inside metadata. Same pattern as Repeater’s metadata.mode.
Adding resolvedReferences for a group-multiple iterationNo harm (the SDK ignores it for non-references), but creates noise + makes the composition look more complex than it isSkip resolvedReferences for group-multiple, multi-file, and scalar-list iteration. Only references need it.
Adding a Condition Block for group-multiple iterationThe block has no discriminator to apply. Runtime behavior may be inconsistentSkip the Condition Block for group-multiple. Place the iteration child directly in the Repeater’s slot.
Using uid instead of queryUID in a contentstack_queries binding valuePinned-query result never resolvesUse queryUID: "<pinned-query-uid>".
Inner Repeater’s items binding uses type: "template" with a nested-.0. path (e.g. { footer_columns: { 0: { links: {} } } })Every outer iteration’s inner-repeater reads column-0’s links, all inner lists identical. The nested-.0. family of the P19 trap, one level up.Inner Repeater’s items MUST use type: "repeater" with repeaterUID: "<outer-uid>" and path { <field-on-outer-iteration>: {} }. Then inner card bindings use the inner Repeater’s UID.
Compositions CT models linked_schemas as a reference field instead of group-multiple (P23)CMA silently drops the {content_type_uid, selected_field} object on every PUT (linked_schemas reads back as []), editor canvas shows blank repeaters on placed sections even though runtime works correctlyModel linked_schemas as a group-multiple on the compositions CT (see provision-studio-project). As a temporary runtime-only fix, set sectionBindingOverride.selectedField on each section-composition node, but the editor canvas won’t render correctly until linked_schemas is also fixed.
A section iterates selectedField and its top-level Repeater binds { path: { <field>: {} } } instead of scope-root { path: {} } (P24)Repeater renders nothing: dataSources.template IS the array (re-scoped via selectedField). template.<field> is undefinedUse scope-root { path: {} } for the top-level Repeater inside a selectedField-scoped section. Leaf bindings inside still use repeater type with repeaterUID.
Setting selectedField on a section that reads multiple page fields (Header reading brand + nav_links + signin_label. Hero reading the hero group)Section loses access to the fields outside the scoped one, they resolve to undefinedLeave selectedField unset for multi-field sections. template becomes the whole page entry. Original template.<field> paths work verbatim.
Authoring a section by API and expecting its own canvas to confirm scopeSection canvas has no data. One empty placeholder is normalVerify via SSR cold-load: await sdk.fetchCompositionData({ url }) then dump spec.data.section_scoped_data[<instance-uid>]. That’s the runtime truth.
API-placing a section in a template’s ui without populating the template’s linked_sections field (P27)”Template Did Not Load” in the Studio editor, BUT the deployed site renders the template correctly via SSR: the editor needs the ?include[]=linked_sections resolution to build spec.sectionCompositions. SSR walks the section-composition nodes directly so it works either wayWhen writing section-composition nodes into a template’s ui, also populate the template entry’s linked_sections field with [{ uid: "<section-comp-entry-uid>", _content_type_uid: "<compositions-CT-uid>" }, …], one entry per distinct placed section (dedupe: a section placed multiple times needs only one reference). Publish the template entry.
Passing the compressed ui inline via a shell variable in curl -d "..."Blank canvas, no error, spec present, hasSpec: true, but every slot resolves empty because shell interpolation mutated "children" into "c1ildren" throughout the treeWrite payload to a temp file, POST with -d @file. Assert raw.count('"children"') >= <expected> before compressing. See § Never pass the compressed ui inline above.
Using Python urllib/requests for CMA calls against a non-prod data centerSSLCertVerificationError: unable to get local issuer certificate, the non-prod chain isn’t in Python’s default CA store. curl reads the system keychain and worksUse curl for the HTTP call. Keep Python for payload build + assertions only. If Python HTTP is unavoidable (dev tooling), pass a permissive ssl.create_default_context() with check_hostname=False + verify_mode=CERT_NONE. Never do this in app code
section-slot node written without slots: { "<own-uid>": [] } (empty-array entry keyed by the node’s own uid)Template’s sectionSlotCompositions has no target to fill: canvas renders the wrapper with no slot placeholder. SSR resolves the slot as absentEvery section-slot node needs slots: { "<own-uid>": [] }, the one legitimate empty-array exception to the preflight rule. See § section-slot node shape
Pattern B fill section’s leaf bindings use type: "repeater" with the wrapper’s repeaterUIDBindings resolve to undefined: the wrapper’s SDK scoping already unwraps the iteration item into the fill section’s template. The wrapper’s repeater scope isn’t visible inside the fill sectionFill section’s leaf bindings use type: "template" with paths relative to the reference target CT / block schema. Only the WRAPPER section uses repeater-scope bindings inside its own tree
Pattern B template drops the section-composition without a sectionSlotCompositions mapWrapper renders its Repeater + CB but each iteration’s section-slot has no fill, placeholder shows blankPopulate metadata.sectionSlotCompositions.<section-slot-uid>.compositionUID with each fill section’s composable_uid on the template’s section-composition node. Add the fill section entry to the template’s linked_sections
Exposed-prop declaration placed on the individual node’s metadata.sectionExposedProps or on the composition entry, instead of ui.metadata.sectionExposedProps at the ui ROOTTemplate’s right panel doesn’t render the field. Overrides never get readPlace sectionExposedProps on the section’s ui ROOT node’s metadata (the top-level page node). It’s an array of {nodeUid, propKey, uid, displayName, propType, bindingAtExposeTime}, one entry per exposure. See § Recipe: Exposed section props
Template’s section-composition.props.<uid> key uses the section’s node uid or prop key instead of the exposure uidOverride never matches: SDK looks up by exposure uid. When unmatched it falls back to bindingAtExposeTime, which looks like the section is ignoring the templateKey the template’s section-composition.props map by the uid field from the section’s sectionExposedProps entry (e.g. "exp-tone-1"), NOT by nodeUid or propKey
Same section dropped twice on a template shares one override valueBoth instances render with the same override, user expected per-instance controlEach drop is a separate section-composition node with its own uid. Each carries its own props.<exposure-uid> map. Static-value keys should also differ per instance (e.g. INST-1-tone, INST-2-tone) so the template’s static_value.<subtype> bucket stores each independently
API-authoring a json_rte embedded-entry reference node with attrs missing localeCMA rejects with "Reference must contain content-type-uid, entry-uid, locale and display-type." at write time. SDK read path doesn’t need locale (derived from context) so this only surfaces on POST/PUTEvery embed reference node needs attrs: { type: "entry", "entry-uid": "…", "content-type-uid": "…", "display-type": "block"|"inline"|"link", "locale": "en-us" }. Applies to both node-level json_rte prop values and static_value.json_rte[*].value embeds. See register-json-rte pitfall row