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.

Field
| Name | Type | Default | Description |
|---|---|---|---|
name | string | — | Field name. Used in {{stores.<store>.<name>}} and set-property. |
type | type | — | See kinds below. |
defaultValue | matches type, or {{…}} | — | Initial value. An expression plus editable: false makes a computed field. |
editable | boolean | true | Set false with an expression defaultValue for derived fields. Do not set-property those. |
id | string | — | Present on top-level store / schema / form fields. Omit on nested object fields and registry slots. |
label | string | — | Editor label. |
validation | object | — | required, min, max, minLength, maxLength, message. |
hidden | boolean | — | Declared but hidden from the props form (className lives on the Style panel). |
options | list | — | Choices for select-style widgets. |
editor | string | — | Widget override: input, multiline, select, image-generate. Not a type. |
discriminator / variants | — | — | Used with kind: "oneof" for a form toggle-group. |
Where types appear
| Surface | What type describes |
|---|---|
Store fields | Runtime state. Read {{stores.<name>.<field>}}; write with set-property. |
Schema fields | A named reusable shape. Reference with { kind: "ref", refId: "<schemaId>" }. |
| Component / primitive registry props | The element's prop bag, including Function slots. |
| Hook config | useForm.fields (same field list); other kinds use a type on their config keys. |
Action params / returns | The store action's signature. Primitive param catalogs are the same field list. |
Hook returns | The object at {{hooks.<alias>.*}}. Callable members are kind: "function" — those become "<hookId>.<method>" actions. |
kind values
kind | Type | Default | Description |
|---|---|---|---|
text | { kind: "text", flavor?: "inline" | "rich" } | — | String. flavor is how much markup the widget allows. |
number | { kind: "number" } | 0 | Number. |
boolean | { kind: "boolean" } | false | Boolean. |
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 / variants | — | Form 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)
| Tool | What you use it for |
|---|---|
list_schemas | List schemas. Pass schemaId for the full field tree. |
create_schema | { name, fields }. Name is PascalCase. |
update_schema | Replaces the whole fields array. |
After create, use the schema id as refId — renaming the schema does not break refs.
| To describe | Type |
|---|---|
| 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:
| Name | Type | Default | Description |
|---|---|---|---|
params | field list | omitted = React event | Declaration order. |
returns | type | omitted = undeclared | { kind: "void" } means returns nothing. |
async | boolean | — | false means the host reads the return now; only a synchronous code-action store action may bind. |
identity | boolean | omitted = wrap as handler | true 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.
| Name | Example |
|---|---|
name | total |
type | { kind: "number" } |
defaultValue | "{{stores.cart.price * stores.cart.quantity}}" |
editable | false |
An expression defaultValue without editable: false is rejected.
Validation
| Name | Example |
|---|---|
name | email |
type | { kind: "text" } |
label | Email |
validation | required: 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.