Skill 12 · Exo Content Bindings
Subchapter 12.4
references/worked-example.mdMarkdown8 KBView on GitHub
This example (from contentful/experiences) shows two coded components hydrated from the same content type. Referenced from .
Fields:
title: Symbol (required)
teaser: Text
ctaLabel: Symbol
ctaUrl: Symbol
image: Link<Asset>contentProperties:
✓ title: String (required)
○ ctaLabel: String
○ ctaUrl: String
○ image: String{
"sys": {
"id": "hero-from-promotion",
"type": "DataAssembly",
"dataType": [
{ "id": "title", "name": "Title", "type": "String", "required": true },
{ "id": "ctaLabel", "name": "CTA label", "type": "String", "required": false },
{ "id": "ctaUrl", "name": "CTA URL", "type": "String", "required": false },
{ "id": "image", "name": "Image URL", "type": "String", "required": false }
]
},
"metadata": { "tags": [] },
"name": "Hero from Promotion",
"description": "Maps a promotion entry into the hero-plain ComponentType",
"parameters": {
"promotionId": {
"name": "Promotion",
"type": "ResourceLink",
"linkType": "Contentful:Entry",
"allowedResources": [{
"type": "Contentful:Entry",
"source": "crn:contentful:::content:spaces/$self/environments/$self",
"allowedTypes": ["promotion"]
}]
}
},
"resolvers": {
"main": {
"source": "Contentful:GraphQL",
"query": "query ($id: ID!) { _node(id: $id) { __typename ... on Promotion { title ctaLabel ctaUrl image { url } } } }",
"parameters": { "id": "$parameters/promotionId" }
}
},
"return": {
"title": { "$from": { "source": "$resolvers/main", "select": "_node/title" } },
"ctaLabel": { "$from": { "source": "$resolvers/main", "select": "_node/ctaLabel" } },
"ctaUrl": { "$from": { "source": "$resolvers/main", "select": "_node/ctaUrl" } },
"image": { "$from": { "source": "$resolvers/main", "select": "_node/image/url" } }
}
}contentProperties:
✓ title: String (required)
○ teaser: String
○ ctaLabel: String
○ ctaUrl: String
○ image: StringSame source content type, different set of mapped fields (adds teaser):
{
"sys": {
"id": "card-from-promotion",
"type": "DataAssembly",
"dataType": [
{ "id": "title", "name": "Title", "type": "String", "required": true },
{ "id": "teaser", "name": "Teaser", "type": "String", "required": false },
{ "id": "ctaLabel", "name": "CTA label", "type": "String", "required": false },
{ "id": "ctaUrl", "name": "CTA URL", "type": "String", "required": false },
{ "id": "image", "name": "Image URL", "type": "String", "required": false }
]
},
"metadata": { "tags": [] },
"name": "Card from Promotion",
"description": "Maps a promotion entry into the card ComponentType",
"parameters": {
"promotionId": {
"name": "Promotion",
"type": "ResourceLink",
"linkType": "Contentful:Entry",
"allowedResources": [{
"type": "Contentful:Entry",
"source": "crn:contentful:::content:spaces/$self/environments/$self",
"allowedTypes": ["promotion"]
}]
}
},
"resolvers": {
"main": {
"source": "Contentful:GraphQL",
"query": "query ($id: ID!) { _node(id: $id) { __typename ... on Promotion { title teaser ctaLabel ctaUrl image { url } } } }",
"parameters": { "id": "$parameters/promotionId" }
}
},
"return": {
"title": { "$from": { "source": "$resolvers/main", "select": "_node/title" } },
"teaser": { "$from": { "source": "$resolvers/main", "select": "_node/teaser" } },
"ctaLabel": { "$from": { "source": "$resolvers/main", "select": "_node/ctaLabel" } },
"ctaUrl": { "$from": { "source": "$resolvers/main", "select": "_node/ctaUrl" } },
"image": { "$from": { "source": "$resolvers/main", "select": "_node/image/url" } }
}
}After creation, each DA is added to its target component type’s dataAssemblies array:
{
"dataAssemblies": [
{
"sys": {
"type": "ResourceLink",
"linkType": "Contentful:DataAssembly",
"urn": "crn:contentful:::experience:spaces/$self/environments/$self/dataAssemblies/hero-from-promotion"
}
}
]
}When a marketer uses this DA in an experience, the node stores:
{
"id": "node:hero",
"nodeType": "InlineExperienceFragment",
"component": {
"sys": {
"type": "ResourceLink",
"linkType": "Contentful:Component",
"urn": "crn:contentful:::experience:spaces/$self/environments/$self/components/hero-plain"
}
},
"contentBindings": {
"sys": {
"type": "ResourceLink",
"linkType": "Contentful:DataAssembly",
"urn": "crn:contentful:::experience:spaces/$self/environments/$self/dataAssemblies/hero-from-promotion"
},
"parameters": {
"promotionId": {
"$literal": {
"sys": {
"type": "ResourceLink",
"linkType": "Contentful:Entry",
"urn": "crn:contentful:::content:spaces/$self/environments/$self/entries/abc123"
}
}
}
}
}
}Each contentBindings.parameters value must be wrapped: { "$literal": <link> } for a fixed entry, { "$from": ... }, or a $parameters/... pointer. A bare { "sys": { "type": "ResourceLink", ... } } value is rejected by the Experiences Management API.
At runtime, ExO executes the DA’s GraphQL resolver with entry abc123, maps the result through the return expressions, and writes the resolved values into the component’s content properties.
sys.dataType mirrors contentProperties — the DA’s dataType IDs must match the component’s content property IDs exactlypromotion feeds both hero-from-promotion and card-from-promotionimage { url } in GraphQL, _node/image/url in the return selectcrn:contentful:::experience:spaces/$self/environments/$self/dataAssemblies/{id}A component type’s dataAssemblies array can hold multiple DAs — each is an alternative binding set. The marketer picks one when configuring an instance:
{
"dataAssemblies": [
{ "sys": { "type": "ResourceLink", "linkType": "Contentful:DataAssembly", "urn": "...dataAssemblies/card-from-promotion" } },
{ "sys": { "type": "ResourceLink", "linkType": "Contentful:DataAssembly", "urn": "...dataAssemblies/card-from-blog-post" } },
{ "sys": { "type": "ResourceLink", "linkType": "Contentful:DataAssembly", "urn": "...dataAssemblies/card-from-product" } }
]
}Each DA is a complete binding set (parameters + resolvers + return). They are not composed together — the marketer selects ONE per instance. Design multiple DAs when:
If two or more component types have the same content property IDs and you want the same source → target mapping, create ONE DA and link it to both component types (see api-workflow.md step 7). Don’t duplicate.
Example: hero and mediaTextRow both have eyebrow, title, description, imageSrc — a single capability-da linked to both is correct.
When to reuse vs. duplicate:
| Situation | Action |
|---|---|
| Same content property IDs, same source content type | Reuse — link one DA to multiple component types |
| Same source content type, different property IDs | Create separate DAs (different return mappings) |
| Same property IDs but different source content types | Create separate DAs (different parameters) |