CodelabDocs
Behavior

Type system

The recursive type tree for store fields, hook fields, schemas, registry props, and callable signatures.

Every typed slot is a field whose type is a recursive type. Leaves are primitives; composites carry payload (object.fields, array.item, ref.refId, and.operands). The discriminator is kind.

The same field shape is used on stores, schemas, components, and form hook fields.

Guest type in the Types inspector
Guest type in the Types inspector

Field

NameTypeDefaultDescription
namestringField name. Used in {{stores.<store>.<name>}} and set-property.
typetypeSee kinds below.
defaultValuematches type, or {{…}}Initial value. An expression plus editable: false makes a computed field.
editablebooleantrueSet false with an expression defaultValue for derived fields. Do not set-property those.
idstringPresent on top-level store / schema / form fields. Omit on nested object fields and registry slots.
labelstringEditor label.
validationobjectrequired, min, max, minLength, maxLength, message.
hiddenbooleanDeclared but hidden from the props form (className lives on the Style panel).
optionslistChoices for select-style widgets.
editorstringWidget override: input, multiline, select, image-generate. Not a type.
discriminator / variantsUsed with kind: "oneof" for a form toggle-group.

Where types appear

SurfaceWhat type describes
Store fieldsRuntime state. Read {{stores.<name>.<field>}}; write with set-property.
Schema fieldsA named reusable shape. Reference with { kind: "ref", refId: "<schemaId>" }.
Component / primitive registry propsThe element's prop bag, including Function slots.
Hook configuseForm.fields (same field list); other kinds use a type on their config keys.
Action params / returnsThe store action's signature. Primitive param catalogs are the same field list.
Hook returnsThe object at {{hooks.<alias>.*}}. Callable members are kind: "function" — those become "<hookId>.<method>" actions.

kind values

kindTypeDefaultDescription
text{ kind: "text", flavor?: "inline" | "rich" }String. flavor is how much markup the widget allows.
number{ kind: "number" }0Number.
boolean{ kind: "boolean" }falseBoolean.
date{ kind: "date" }Date.
file{ kind: "file", accept?: string }A file value { fileId, name?, format? } — not a URL string. accept is a MIME glob (image/*, video/*). An expression that evaluates to a URL is also legal here because the vendor slot is src.
object{ kind: "object", fields }Nested record. fields is a field list. Holds a value of a shape.
array{ kind: "array", item: type }List. item is any nested type. UI label: List.
ref{ kind: "ref", refId: string }An app schema. UI label: Custom Type.
and{ kind: "and", operands: type[] }Intersection. Last operand wins on the same field name. UI label: Combined Type.
or{ kind: "or", operands: type[] }Type-level union (string | string[] | void). No stored discriminator.
oneof{ kind: "oneof" } plus field discriminator / variantsForm toggle-group: named variants, each with fields. Distinct from or.
element{ kind: "element" }{ elementId }. The renderer hands the vendor a ref; dispatch resolves it to the DOM node (e.g. toast anchor).
component{ kind: "component" }{ componentId, instanceProps? }.
function{ kind: "function", params?, returns?, async?, identity? }A function call { functionId, instanceParams?, when? }. See below.
renderprop{ kind: "renderprop", params }Nothing is stored on the slot. The element's children are the callback body; declared params (a field list) bind as {{render.<name>}} (typically item / index). Return is rendered, not consumed as a value.
void{ kind: "void" }Nothing. Only meaningful as a callable's returns. Distinct from omitted returns (undeclared). UI label: Nothing.
interface{ kind: "interface" }The value is a field list (a shape). useForm's fields config is this: Object holds a value of a shape; Interface holds the shape.

Schemas (custom types)

ToolWhat you use it for
list_schemasList schemas. Pass schemaId for the full field tree.
create_schema{ name, fields }. Name is PascalCase.
update_schemaReplaces the whole fields array.

After create, use the schema id as refId — renaming the schema does not break refs.

To describeType
That schema{ kind: "ref", refId: "<schemaId>" }
A list of that schema{ kind: "array", item: { kind: "ref", refId: "<schemaId>" } }
That schema plus extra fields{ kind: "and", operands: [ { kind: "ref", refId }, { kind: "object", fields } ] }

A refId that transitively points back at this schema is rejected. Dangling refs are not cycles.

update_schema wholesale-replaces fields. list_schemas first, then send the whole array.

kind: "function" — the signature

A Function type carries an optional signature:

NameTypeDefaultDescription
paramsfield listomitted = React eventDeclaration order.
returnstypeomitted = undeclared{ kind: "void" } means returns nothing.
asyncbooleanfalse means the host reads the return now; only a synchronous code-action store action may bind.
identitybooleanomitted = wrap as handlertrue passes the compiled store callable (filter, validate, …).

The value on that slot is always a function call, never a body. See Function.

A store action or primitive that knows its shape carries the non-partial signature (params / returns on the action; param catalogs on primitives).

Literal vs expression values

A field's defaultValue (and any prop / instanceParams value) is a literal or an expression — type says what the value is, not whether it is bound.

A whole-binding string "{{stores.cart.price}}" is stored as an expression. Mixed text interpolates as a string.

A field is computed when that expression is paired with editable: false. The pairing is the discriminator — computed is not its own kind. The store keeps the expression as the field's value and evaluates it on every {{stores.*}} read. set-property must not write it.

NameExample
nametotal
type{ kind: "number" }
defaultValue"{{stores.cart.price * stores.cart.quantity}}"
editablefalse

An expression defaultValue without editable: false is rejected.

Validation

NameExample
nameemail
type{ kind: "text" }
labelEmail
validationrequired: true, message: "Email is required"

update_store wholesale-replaces fields (and actions). update_hook(config) wholesale-replaces the hook config, including useForm.fields. Always list_* first, then send the full array.

On this page