Codelab
Model

Component

A reusable element tree with a declared prop schema.

A Component is a reusable element tree you define once and place many times. Identity + fields (the prop schema) + testData for the canvas. The tree is loaded with the component when you outline or edit it.

packages/modules/component/core/src/domain/component.model.types.ts

It is not an element. An element on a page can be a component instance: kind: { _tag: "Component", value: "<componentId>" }. That node's props are the instance values (title, events, …). The definition lives here; the instance is a page (or parent component) node.

What it is

FieldMeaning
name / description / thumbnailCatalog identity. list_components matches search against name and description.
layershell | rail | panel | section | control.
categoryDiscovery group: layout, form, navigation, overlay, …
fieldsDeclared prop schema — what a call site may pass. Same field shape stores use.
testDataSample values for those fields while editing the definition. Preview with nothing bound falls back here.
isPublishedWhen true, other apps may reference this component by id (no copy). Owner-only toggle.

packages/modules/component/kernel/src/ui-layer.enum.ts packages/modules/component/kernel/src/component-category.enum.ts

create_component starts with no elements. A page scaffolds a body root; a component does not — the root kind is yours. Seed it with add_root_element, then add_element for children. The root kind must be a registered Primitive (header, div, section, …), not a Component reference.

packages/modules/component/server/src/services/create-component.service.ts packages/modules/element/adapter/src/ai/add-root-element.tool.ts

Optional fields on create are the prop schema — the same array set_props_schema would write. Do not follow create with set_props_schema just to re-declare them.

packages/modules/component/adapter/src/ai/create-component.tool.ts

Inside the tree, {{props.title}} is this component's RuntimeContainer. Entering the component replaces the outer frame — an inner {{props.X}} never reaches the page's props. Pass page or store data in at the instance (update_props on the Component-kind element), then bind_prop (or a {{props.*}} expression) onto inner nodes.

packages/modules/container/core/src/domain/runtime-container.model.ts packages/modules/expression/core/src/domain/expression.namespaces.ts

bind_prop writes a tagged expression onto a target prop:

{ elementId, propKey: "title", targetProp: "children" }
→ element.props.children = { _tag: "Expression", source: "{{props.title}}" }

packages/modules/props/adapter/src/ai/bind-prop.tool.ts

A store can attach to the component (container: { _tag: "Component", id }) so state travels with it and the name state stays free.

packages/modules/store/adapter/src/ai/create-store.tool.ts

How it relates

  • App — components are listed and created in the active app (contextId).
  • Page — place the component as an element. Set instance values with update_props (or instanceProps on add_elements).
  • Element — the definition is an element tree (container: { _tag: "Component", id }). Same Primitive vs Component kind, props, styles, repeat.
  • Page vs component as containers — both are RuntimeContainer. Only the component side is what you usually declare fields for; pages also have author fields, plus platform id / appId / params / search.

MCP tools

packages/modules/mcp/server/src/component.mcp.ts

ToolWhat it does
list_components{ search? } — id, name, description, layer, category, isPublished. Search immediately before create so you do not duplicate.
load_component_outlineSame indented outline as load_page_outline, plus fields and testData.
find_element_in_componentSearch by aria-label, text, or kind inside this tree.
create_component{ name, description?, layer, category, fields? }. Empty tree.
update_componentMetadata + isPublished. Not schema, not test data, not the tree.
delete_componentDeletes the definition and the elements it owns (component_id cascade). Page instances that reference it are not removed — remove_element those.
set_props_schemaReplaces the whole fields array. Remember the keys for bind_prop / set_test_data.
set_test_dataSample values keyed by those prop names.
bind_propWire {{props.<key>}} onto an inner element's prop.

Tree mutations use the element tools with container: { _tag: "Component", id: componentId }.

Build sequence:

list_components { search: "ProductCard" }     # skip create if it exists
create_component { name: "ProductCard", layer: "section", category: "layout",
                   fields: [{ name: "title", type: { kind: "text" } }] }
add_root_element { container: { _tag: "Component", id }, kind: { _tag: "Primitive", value: "article" } }
add_element      … children …
bind_prop        { propKey: "title", targetProp: "children" }
set_test_data    { title: "Summer festival" }

On a page:

add_element {
  container: { _tag: "Page", id: pageId },
  kind: { _tag: "Component", value: componentId },
  parentElementId: "…"
}
update_props { elementId, commands: [{ op: "set", key: "title", value: "Ceremony" }] }

Gotchas

  • Empty until you seed a root. add_element needs an existing parent and errors on an empty tree. add_root_element is the first call; pages never need it. (packages/modules/element/adapter/src/ai/add-root-element.tool.ts)
  • set_props_schema is wholesale. list / load_component_outline first, then send the full fields array. Same pattern as update_store. (packages/modules/component/adapter/src/ai/set-schema-fields.tool.ts)
  • Search, then create. list_components({ search }) is a substring on name and description. A listing from earlier in the session is how duplicates get made. (packages/modules/component/adapter/src/ai/list-components.tool.ts)
  • Props do not leak inward. Bind signedIn: "{{session.signedIn}}" (or a store field) on the instance, then renderIf: "{{props.signedIn}}" inside. (packages/modules/container/core/src/domain/runtime-container.model.ts)
  • instanceProps only on Component kinds in add_elements. Primitive nodes use props. Both fold into the same record the renderer reads. (packages/modules/element/adapter/src/ai/add-elements.schema.ts)
  • Same tree rules as pages. Purpose aria-label, omit targetPosition to append, inspect_element_type before composite primitives. See Element.

On this page