---
name: author-composition-pitfalls
---

## 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`](author-composition-via-api.md), 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:

```jsonc
"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`](decompose-design.md) § 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

| Pitfall | Symptom | Fix |
|---|---|---|
| Rebuilding a template's root slot from a fresh node list | Sibling 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 it | Rebuild 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 name | Two scopes collapse to one title. Titles are unique per project, so the second create returns `409` and that Section is missing from the template | Keep the title 1:1 with the scope it is rooted at |
| Creating a Section and placing it in the same pass without publishing | The Section resolves to nothing at delivery and the page renders `Component with type '<uid>' is not registered` | Publish every Section before the Template that references it. See [`complete-the-build`](complete-the-build.md) |
| Missing `data_sources.resolvedReferences` | Repeater renders 0 items, reference stubs never materialise | Add the resolution map entry: `{ uid:"template", resolvedReferences: { "root.<refField>": ["<refField>"] } }` |
| Authoring from the registration without reading the component source | The 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 error | Open 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 working | Leave 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 values | A 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 plausible | Treat 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 reads | A 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 element | Read the component's first lines: early returns name the prop it actually requires |
| Writing an `object` or `array` value as a real structure | Those buckets store a **JSON string**. A real object fails the whole entry with `422 static_value.object.N.value is not text` | `json.dumps` the value before writing |
| Relying on `height: 100%` inside a composition | The 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 card | Use 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 nodes | The 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 nested | Put 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 height | Distribution 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 bottom | Give 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 width | `justify` 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 children | Give 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 string | The 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 divider | Its 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 heading | Read 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 primitive | A `Deviations` row that says impossible stops anyone looking again, so an expressible value stays unbuilt indefinitely | Search the registry for shape/box primitives with raw CSS `color` / `width` / `height` first |
| `choice` value stored as a bare string instead of a list | **Renders 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 dictates | Every 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 work | Write every registered prop: comp value, else the registration's own `defaultValue`. See § Node anatomy point 0b |
| Nodes with no `metadata.title` | The Layers tree shows repeated type names (`Stack / Box` ×6) and cannot be navigated | Give each node a purpose-name: `Stack — Card content`, `CTA — Read the full story` |
| Leaf node authored without `metadata` / `slots` | Whole canvas shows `Component Loading Error — Cannot read properties of undefined (reading 'visible')`. Nothing paints and the message names no node | Every 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 skeleton | Write `{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 section | Section renders with the library's defaults, not the comp: a stack defaults to `column`, so side-by-side CTAs stack. No error | Author 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 N | Set `metadata.mode: "preview"` on the Repeater node |
| Missing Condition Block child of Repeater (reference / modular-block iteration) | Iteration silently fails to resolve child bindings | Insert 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 for | Always 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](author-composition-entry.md#url-metadata-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 trace | Verify 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 API | Reverts: `url_queries` is only generated by the UI's Edit-URL then Save flow, and Studio re-derives from the CT pattern on editor load | Make 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_preview` | Blank/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 UI | Re-save each section in Studio (backfills automatically), or upload an asset + `PUT {"entry":{"ui_preview":"<assetUid>"}}` + publish **both** asset and entry. See § [Section thumbnails](author-composition-thumbnails.md#section-thumbnails-ui-preview) |
| `ui_preview` set to an unpublished asset uid | Same blank tile: the accordion resolves `ui_preview.url` against the delivery environment and gets nothing. Looks identical to never setting the field | Publish the asset to the environment the Studio project targets, not just the entry |
| Template places a section by its `composable_uid` | `Component with type '<it>' is not registered`: sections are looked up by entry uid | Set `metadata.compositionUID` to the section's entry uid (`blt…`) |
| Authored composition is unpublished | Not visible in compositions list. Can't load in canvas | Publish the entry. Studio queries by `composable_uid` AND published state |
| `linked_schemas` modeled as a reference field on the compositions CT instead of group-multiple | Section'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 guide | Wastes time. Introduces shape inconsistencies | Use only for headless / scripted / seed / migration work. For interactive authoring, the Data Picker writes every shape correctly. |
| Cross-stack migration: keeping the source entry uids in the template | Every section is "not registered": the template still points at source-stack entry uids | Rewrite the template's `ui` `compositionUID`s + `linked_sections` to the destination entry uids. See `author-composition-migration` |
| Cross-stack migration: copying binding index paths / `selected_field` verbatim | Destination 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 error | Diff 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.resolvedReferences` | Scalars 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 error | Add 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](author-composition-repeater-recipes.md#reference-path-termination) |
| Linking a **DAM / Assets-Manager** asset (`am…` uid, `parent_uid`) to a `file` field via **OAuth Bearer** CMA write | Rejected with `422 "is not a valid upload."`: the Bearer app-token grant can't link DAM assets, even though the asset exists + is published | Use 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>.binding` | The SDK can't find the binding, so the prop renders with `defaultValue` or empty | Always wrap: `"props": { "<propName>": { "binding": { type:…, value:{…} } } }`. |
| Using `children: [...]` instead of `slots: { <slot-uid>: [...] }` for child nodes | Children invisible to the renderer. Canvas renders the parent as empty | Use `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 resolve | Put `condition` inside `metadata`. Same pattern as Repeater's `metadata.mode`. |
| `metadata.condition` without `conditionBinding` | Block renders nothing, no error. Design mode still shows it | Add it (`author-composition-repeater-recipes` step 4). Verify in preview. |
| Adding `resolvedReferences` for a group-multiple iteration | No harm (the SDK ignores it for non-references), but creates noise + makes the composition look more complex than it is | Skip `resolvedReferences` for group-multiple, multi-file, and scalar-list iteration. Only references need it. |
| Adding a Condition Block for group-multiple iteration | The block has no discriminator to apply. Runtime behavior may be inconsistent | Skip 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 value | Pinned-query result never resolves | Use `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 correctly | Model `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 undefined | Use 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 undefined | Leave `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 scope | Section canvas has no data. One empty placeholder is normal | Verify 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 way | When 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 tree | Write 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 center | `SSLCertVerificationError: unable to get local issuer certificate`, the non-prod chain isn't in Python's default CA store. `curl` reads the system keychain and works | Use `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) | The template has no slot uid to fill under: canvas renders the wrapper with no slot placeholder. SSR resolves the slot as absent | Every `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 `repeaterUID` | Bindings 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 section | Fill 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 fill written as `metadata.sectionSlotCompositions` on the placed wrapper | The SDK never reads that key, so each iteration's slot renders blank | Put the fill in the wrapper's `slots`: `slots.<section-slot-uid>` holds a `section-composition` node with `metadata.compositionUID` = the fill section's entry uid. 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 ROOT | Template's right panel doesn't render the field. Overrides never get read | Place `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 `uid` | Override never matches: SDK looks up by exposure `uid`. When unmatched it falls back to `bindingAtExposeTime`, which looks like the section is ignoring the template | Key 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 value | Both instances render with the same override, user expected per-instance control | Each 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 `locale` | CMA 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/PUT | Every 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`](register-json-rte.md) pitfall row |
