Skills
Skill 12 of 12
Create Data Assemblies (content bindings) in a Contentful Experience Orchestration space, connecting existing entries to component content properties.
13 minutes · 2,812 words · 17 sections
Install
npx skills add contentful/skills --skill exo-content-bindingsnpx skills add contentful/skills/plugin marketplace add contentful/skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
Creates Data Assemblies that connect existing Contentful entries to Experience Orchestration component types. A Data Assembly is a parametrized query with a known return type — it describes how entry fields hydrate a component’s content properties.
Mental model — four layers:
| Layer | Entity | What it does | Example |
|---|---|---|---|
| Schema | Component Type / Template | Declares contentProperties — named “holes” expecting content | heading, image, ctaLabel |
| Recipe | Data Assembly (API) / Binding Set (UI) | Describes how entries fill those holes (resolvers + return map) | “Product Card from Product” |
| Instance | Experience / Fragment / Inline Component | Stores contentBindings — a chosen DA + parameter entries | this card uses productEntry123 |
| Runtime | Delivery payload | Resolved values written into content properties | heading = "Spring Sale" |
This skill operates at the Recipe layer — creating and publishing Data Assemblies.
The invariant that makes the model work: the component tree always reads through $contentProperties/... — never directly from an entry, never from $contentBindings/.... Every binding strategy exists to fill a content property. A binding that fills no content property computes data nothing reads. Use this as a correctness check on any DA you design: trace each return key to a declared content property, or delete it.
The practical consequence: from the component’s point of view, a manual value and a DA-hydrated value are indistinguishable. It just reads a content property. That is why the DA-vs-literal choice is a modeling decision, not a rendering one.
Precise language prevents misunderstandings between these easily conflated concepts:
| Term | What it is | What it is NOT |
|---|---|---|
| Content property | A named input on a Component Type or Template that receives content | Not a content-type field (those are the source) |
| Content-type field | A field on a Contentful content type (the CMS data) | Not a content property (those are on ExO components) |
| Parameter | One marketer selection input on a DA, constrained to allowed content types | Not a binding — it’s the input to a binding |
| Field mapping | A target content property + source path starting from a parameter | Not the parameter itself |
| Binding set | The UI/editor name for an editable data-wiring configuration that corresponds 1:1 with a Data Assembly. Use “Binding Set” when referencing editor UI; use “Data Assembly” when referencing API calls. | Not the DA entity itself — same thing, different contexts |
| Prebinding | The ComponentType editor workflow where DAs are defined. This skill automates what Prebinding does in the UI. | Not the runtime resolution step |
| Content binding | An instance choosing a DA + providing entry IDs for its parameters (at the Experience/Fragment level) | Not the DA itself (that’s the recipe) |
| Nested binding | A PropertyMapping that gives a content property the whole output of a child DA through a DataAssemblyResolver | Not a reference field traversal |
| Reference path | Zero or more reference fields followed by a terminal value field | Not a display label |
| Template | A page-level entity governing Experiences (has channel config). Can be coded or composite, same as component types | Not interchangeable with “component type” — templates have channels/viewports |
This skill calls the Contentful Management API (CMA) directly. Prefer curl (via Bash) when a shell is available — the examples in references/ use it. If no shell/exec tool is available, make the equivalent calls through the contentful-mcp MCP tools instead (same endpoints and payloads, translated to whatever tool the server exposes for spaces/component types/data assemblies). If neither curl nor contentful-mcp is reachable, say so explicitly and stop — do not guess at space state or fabricate a plan without introspecting first.
A content property is content, not presentation. Before designing any mapping, apply this test to each property:
Apply the test explicitly rather than guessing from the property name, and rather than relying on which array the property appears in. variant, columns, backgroundColor, showDivider, theme, alignment are design properties even when their values happen to come from the CMS. A theme: light|dark field on a content type is the classic trap: it is CMS-authored, so it looks bindable, but it changes how the component looks, so it is not a content property and must not appear in a DA’s return map.
The converse also holds: a property whose name sounds decorative (badgeText, eyebrow, label) is a content property if changing it changes what the page says.
This test governs field-mapping inference (references/api-workflow.md step 5.3). A source field that fails it is not an unmapped content property — it is correctly unmapped, and should be reported that way in the Binding Plan rather than listed as a gap.
Full CMA call sequences, request/response shapes, and exact commands for every step: references/api-workflow.md (opens in a new tab). Full resolver request bodies (entity, collection, nested-DA, asset/Media, deep binding, polymorphic): references/resolver-patterns.md (opens in a new tab). Pointer expressions, GraphQL conventions, RichText handling, and the type mapping table: references/richtext-and-types.md (opens in a new tab). A complete worked example end to end: references/worked-example.md (opens in a new tab).
Not every content property in a composite is reachable from outside.
Public properties are the subset exposed on the component’s surface. Only public properties can be overridden by a parent embedding this component, targeted by a Data Assembly, or seen and edited by a marketer. They are the component’s stable content input contract.
Private properties belong to child components inside the componentTree and are not exposed. There is no privateProperties array — privacy is defined by absence: a child property is private precisely because it was not hoisted. Do not go looking for a list of private properties; enumerate the tree’s children and subtract what appears in the parent’s contentProperties.
The golden rule: never bind, override, or map a property that is not public. If something must be bound, it has to be hoisted first — which is a component-type change, outside this skill’s scope.
contentProperties, it’s private — the component type must be updated first (out of scope)TypeRef) are mutually exclusive for the same childRules 1 and 3 can contradict each other — say so when they do. If a child property is required on the child but absent from the parent’s contentProperties, rule 1 says it should have been hoisted automatically. Do not silently record it as “intentionally private.” Report it as a possible modeling or auto-hoisting defect for the component-type owner, alongside the blocked mapping. A required-but-unreachable property means the composite cannot render correctly from any binding.
You cannot change hoisting from this skill, but you will be asked whether something should be hoisted. Recommend hoisting when any of these hold:
Recommend keeping it private when:
text1, text2, url1, url2… clutter, which is explicitly discouraged,Default posture: hoist the minimum that satisfies the four triggers; leave everything else private. A small, intentional public surface is the goal.
Slots create a hard DA ownership boundary:
Parent DA scope:
├── parent's contentProperties (direct + hoisted) ← DA targets these
├── componentTree children read via $contentProperties/... ← fed by parent DA
└── [SLOT: "main"]
└── Child components placed here ← SEPARATE DAs, not parent'sNever attempt to map fields into a slot child’s properties from the parent’s DA.
Both are reference-shaped. They mean opposite things, and confusing them produces a structurally wrong experience.
| Slot | Content binding | |
|---|---|---|
| Means | “this component contains these other visual components” | “this component reads data from these records” |
| Referenced thing | becomes a Fragment inside this component | stays content, accessed through a Data Assembly |
| Target shape | presentational — has its own layout, design properties, nested references | data-oriented — flat domain data, no layout |
How to tell: look at what the reference actually points to. If the target has its own visual structure (layout fields, design properties, nested presentational references), it is a slot. If it is flat domain data — categories, tags, profiles, events — referenced by many different types as a shared source, it is a binding.
The same distinction routes the request you were given. “Fill the slots” is ambiguous and resolves by context:
Phrases like “fill unlinked slots”, “populate empty slots”, “fill in missing entries” with no structural intent mean content binding on instances, not assembly — and not this skill either. This skill builds the recipes; it does not attach bindings to Experience or Fragment instances (see Limitations).
So when a request contains that phrasing, name it rather than only declaring it out of scope. Say which layer it belongs to, confirm that the DAs this skill creates are the prerequisite for it, and state what remains to be done afterwards — per instance, in the editor or via the instance API. Answering “out of scope” without naming the layer leaves the user unable to act.
You MUST produce a complete binding plan and get user approval before creating any Data Assemblies. Do not skip this step. Do not create DAs incrementally without a plan. See references/api-workflow.md (opens in a new tab) §5.4–5.5 for the plan format.
Steps 0a and 0b decide whether to bind. Steps 1–3 decide how. Do not start at Step 1.
Step 0a: Is this request even about Data Assemblies?
"Bind entries to components" / "wire up content" / "create DA"
→ YES, this skill. Continue.
"Fill the empty slots" / "populate this page" / "fill in missing entries"
→ Instance-layer content binding. This skill creates the prerequisite
recipes but does NOT attach bindings to instances. Name the layer,
create the DAs, then say what remains per instance.
"Place these components into the template" / "build the tree"
→ Assembly, not binding. Not this skill.
Step 0b: Is this property a content property?
Changing the value changes WHAT IS SAID → content property, continue
Changing the value changes HOW IT LOOKS → design property, STOP.
Never bind it. Report as correctly-unmapped, not as a gap.
(theme, variant, columns, alignment, backgroundColor, showDivider —
still design properties when the value comes from the CMS)
Is it public (present in the component type's contentProperties)?
NO → STOP. Golden rule: never bind a non-public property.
Required on the child but absent here? → flag as a possible
auto-hoisting defect, not as intentionally private.
Step 1: What kind of component type?
CODED → all contentProperties are public, go to Step 2
COMPOSITE → check contentProperties list:
Has contentProperties? → go to Step 2 (they're all public/hoisted)
No contentProperties? → this composite doesn't need a DA
(content arrives via slot children, each with their own DA)
Step 2: What feeds this content property?
Single entry's field(s)?
YES → Entity resolver (kind: "entity", _node query)
Needs fields from a linked entry (reference)?
YES → How complex is the linked entry?
Simple (1-2 fields) → Scalar deep bind (traverse ref in GraphQL query)
Complex (own component) → TypeRef deep bind (nested DA)
Limit: ≤ 3 hops from root entry
NO → Single GraphQL resolver (shallow bind)
Array of entries?
YES → Collection resolver (kind: "collection", ...Collection query)
Fixed value?
YES → Use $literal in return (no resolver needed)
Polymorphic source (multiple possible types)?
YES → Use $on to branch on __typename
Step 3: Is this property on a slot child?
YES → Create a SEPARATE DA for that child (do not include in parent DA)
NO → Include in the component's own DA| Mistake | Fix |
|---|---|
| Not classifying coded vs. composite first | Always classify — composites may not need a DA at all |
| Building a DA for a composite that only has slot children | If all content comes via slots, each child gets its own DA — parent needs none |
| Trying to hydrate slot children from the parent’s DA | Slot boundary is absolute — each slot child needs its own DA |
Using bare string pointers in return without checking reference | Default to $from syntax — bare pointers work but aren’t editable in BindingPanel. Replicate reference space style if one exists. |
Missing __typename in GraphQL query | Required for $on branching at runtime |
| Using CMA field IDs directly in GraphQL | GraphQL uses camelCase (e.g., internalName not internal_name) |
Forgetting kind: "collection" for array resolvers | Without it, resolver returns single object not array |
Setting allowedTypes to content type name instead of ID | Must be the programmatic ID (e.g., topicProduct not Topic Product) |
| Creating DA with same ID without version header | Fetch version first, or use upsert pattern (see references/api-workflow.md (opens in a new tab)) |
Mapping a property that isn’t in contentProperties | Only the component’s declared public properties are bindable |
| Hoisting per-property AND whole-component for same child | Mutually exclusive — pick one strategy |
| Not linking DA to the component type | After creating, add DA to the component type’s dataAssemblies array — without this, it won’t appear in the binding panel |
| Not publishing the DA after creation | Unpublished DAs not available for binding in editor |
sys.dataType IDs don’t match contentProperties IDs | They must be identical — the DA’s dataType is the contract between resolver output and the component’s input |
Modeling images as bare String (URL only) | Use Record with url/width/height/alt/contentType |
Mapping asset alt from the asset’s title | alt comes from description, falling back to title only when description is empty across the sample — and say so in the Binding Plan when you fall back |
| Binding a design property because its value comes from the CMS | Apply the what-is-said / how-it-looks test. theme, variant, alignment are design properties regardless of source |
| Recording a required-but-unhoisted child property as “intentionally private” | Required properties are auto-hoisted — absence is a possible modeling defect. Flag it |
| Mapping a low-confidence guess to fill out the plan | Leave it unmapped and name it. A wrong mapping produces plausible content in the right shape, so nobody notices it |
| Answering “out of scope” to a “fill the empty slots” request | Name the layer it belongs to and what remains after the DAs exist |
Using $from syntax for RichText fields | RichText requires bare string pointers to the parent object. $from causes “expected RichText, got String” |
Declaring RichText as String dataType to avoid validation | Use RichText dataType + { document: json } alias + bare parent pointer. The platform resolves it natively |
Pointing to /json or /document leaf for RichText | Point to the parent (e.g., $resolvers/main/ct/subline). The platform reads the aliased document key itself |
Omitting sys.id from PUT body | API rejects with “Invalid input at sys.id” — always include "id": "$DA_ID" in the sys object |
| Creating duplicate DAs for components sharing the same content property IDs | Create ONE DA and link it to multiple component types (see references/worked-example.md (opens in a new tab)) |
_node/headline must correspond to an actual field on the source content type._node/author/name means “follow the author reference, then read name“. Each segment must be a valid field on the resolved type at that depth.contentProperties array, it cannot be targeted regardless of whether it exists internally. Never bind, override, or map a property that is not public.theme, variant) are still design properties.return key fills a declared content property — the tree reads only through $contentProperties/..., so a mapping that fills nothing is dead weight, not a spare.$on type discriminator uses GraphQL __typename (PascalCase of content type ID)contentProperties don’t need DAs — all content flows through slot childrendescription → title) cannot be expressed in a DA. They are resolved at design time by sampling; see references/resolver-patterns.md (opens in a new tab) “Asset (Media) fields”allowedTypes filters by content type only. If publication state matters for a component, it has to be handled by the marketer at bind time or by the source content model, not herecontentful-guide skill — general Contentful concepts and API routingcontentful-migration skill — content model/schema changes (hoisting a property requires a component-type change, which is out of this skill’s scope)The procedure, resolver patterns, GraphQL conventions, and API call sequences here were developed and validated against live Contentful spaces. The judgment layer — the content-vs-design test, the public/private golden rule, “should a property be public?”, the slot-vs-content-binding distinction and request routing, and source-selection confidence — reflects hoisting and content-binding rules defined by Contentful’s Experience Orchestration platform. Where this file and the platform’s own behavior disagree, the platform wins — report the discrepancy rather than following this file.
Create Data Assemblies (content bindings) in a Contentful Experience Orchestration space, connecting existing entries to component content properties. Covers classifying coded vs. composite component types, judging content vs. design properties, public vs. private (hoisted) properties, slot vs. content-binding scope, and designing, creating, linking, and publishing Data Assemblies via the CMA. Use when asked to create Data Assemblies, wire up content, bind entries to components, hydrate a component, or set up a Binding Set. Also triggers on "content binding", "data assembly", "prebinding", "create DA". Not for placing components into a template's structural slots (assembly, not binding) or attaching bindings to an existing Experience/Fragment instance — this skill creates reusable recipes, not per-instance bindings.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
Bash(curl*)mcp__contentful-mcp__*mcp__plugin_contentful-skills_contentful-mcp__*skills/exo-content-bindings/SKILL.mdmain, last pushed 8 November 2026.SKILL.md, not by matching a directory convention. 3 distinct layouts observed: local-skills/skills/*/SKILL.md, skills/*/SKILL.md, skills/contentful-apps/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by Contentful, declaring 1 plugin. It is read for editorial metadata only — never as the skill index, which is always the repository tree./contentful/skills.md, and each skill at its own .md URL.5 files · 61 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 12.
Documentation the agent loads on demand, rather than up front.
Everything else published alongside the skill.