Entry fields
A uicast document is JSONLines: one JSON object per line, each an entry describing one element. The engine mounts entries into a tree by key reference. The dynamic fields are JavaScript expressions over the document’s scopes.
Every field, used once:
{"key":"task-card","component":"Card","props":{"literal":{"title":"Tasks"}},"seed":[{"set":"scopes.root.tasks","expr":"listTasks()"}],"loading":"scopes.root.busy","children":["task-rows","empty-note","refresh"]}
{"key":"task-rows","component":"Typography","each":"scopes.root.tasks","as":"task","keyBy":"id","props":{"expr":"({ text: scopes.task.title })"}}
{"key":"empty-note","component":"Typography","hidden":"scopes.root.tasks.length > 0","props":{"literal":{"text":"No tasks yet"}}}
{"key":"refresh","component":"Button","props":{"literal":{"text":"Refresh"}},"callbacks":{"onClick":[{"set":"scopes.root.busy","literal":true},{"set":"scopes.root.tasks","expr":"listTasks()"},{"set":"scopes.root.busy","literal":false}]}}task-card is the root: no children array mentions it.
interface ComponentEntry {
key: string; // unique id — required
component: string; // catalog component name — required
props?: ValueSource; // the props object
hidden?: Expression; // truthy → hidden
loading?: Expression; // truthy → busy
children?: string[]; // child keys, in render order
seed?: ValueSourceAssignment[]; // initial state, once at mount
callbacks?: Record<string, CallbackValueSourceAssignment[]>; // event → steps
each?: Expression; // array to iterate — its presence marks a list
as?: string; // names the item's scopes
keyBy?: string; // per-item identity field
}Structure
key
Required. A unique id, in kebab-case ("user-card", never
"userCard"). Parents name children by it, and re-emitting a key
replaces that subtree in place.
{ "key": "user-card", "component": "Card" }component
Required. The catalog name, matched exactly against the renderer’s registry:
{ "key": "user-card", "component": "Card" } // ✅ in the catalog
{ "key": "user-panel", "component": "UserCard" } // ❌ Unknown component: UserCardAn unknown name renders the element’s error slot; the rest of the document is unaffected.
children
The keys of the entries inside this one, in the order they render. An entry
with nothing inside has no children:
{"key":"row","component":"FlexRow","children":["name","email"]}
{"key":"name","component":"Typography","props":{"literal":{"text":"Ada Lovelace"}}}
{"key":"email","component":"Typography","props":{"literal":{"text":"ada@example.com"}}}children is reserved: a definition may not declare it as a prop. Text
goes in a prop named for what it holds, text above.
Value and state
props
A value source giving the props object, static or computed (wrap an object literal in parens). It is a reactive site: the engine subscribes to every path the expression reads. See Reactivity.
{ "props": { "expr": "({ text: scopes.root.title })" } }
{ "props": { "literal": { "variant": "outline" } } }Props that fail the definition’s schema raise invalid-props in the
element’s error slot, before render.
hidden
A bare expression, with no literal form, that hides the element while
truthy. Reactive, like props.
{ "key": "details", "component": "Typography", "hidden": "!scopes.root.showDetails",
"props": { "literal": { "text": "Ships in 2 days." } } }hidden does not unmount: it hides the subtree inside React’s
<Activity>.
loading
A bare expression, like hidden. While it is truthy the element is busy: the
React binding hands it to the implementation as
context.loading. Set it from the callback that
refetches:
{ "key": "rows", "component": "Table", "loading": "scopes.root.busy", "children": ["head", "body"] }
{ "key": "search", "component": "SearchInput", "callbacks": { "onChange": [
{ "set": "scopes.root.q", "expr": "evt.value" },
{ "set": "scopes.root.busy", "literal": true, "debounce": true },
{ "set": "scopes.root.rows", "expr": "listRows({ q: scopes.root.q })" },
{ "set": "scopes.root.busy", "literal": false }
] } }The host call is a barrier, so the flag clears after the rows land. A first
load through seed shows the skeleton instead; loading
is for refreshes.
seed
An array of assignments that initialize scope paths once, when the element first mounts.
{ "seed": [{ "set": "scopes.root.users", "expr": "getUsers()" }] }-
Writes only undefined fields. A step skips its write when its
setfield already holds a value, so a seed never overwrites state. -
Seed state, not derivations. Seed values that change later: form fields, selections, fetched lists. Compute derived values in
props,hiddenoreach; a seed would freeze them at mount time. -
Dependent seeds work. Steps run in dependency waves:
{ "seed": [ { "set": "scopes.root.tz", "literal": "UTC" }, { "set": "scopes.root.users", "expr": "getUsers()" }, { "set": "scopes.root.selected", "expr": "scopes.root.users[0].id" } ] }tzandusersrun in parallel.selectedreads whatuserswrites, so it waits forgetUsers(). See lifecycle.
callbacks
A map from event name to an ordered list of steps, run when the component fires that event. A step is an assignment, optionally confirmed or debounced.
{ "callbacks": {
"onChange": [
{ "set": "scopes.root.q", "expr": "evt.value" },
{ "set": "scopes.root.results", "expr": "searchTasks({ q: scopes.root.q })" }
]
} }-
Steps run in dependency order. A step that reads a path an earlier step wrote waits for it; independent steps run in parallel; a host-function step runs alone in its wave.
-
evtis the event payload, shaped by the component’s definition. -
currentValueis the value now at the step’ssetpath, andconfirmgates a step behind a Yes/No prompt:{ "callbacks": { "onClick": [ { "set": "scopes.root.expanded", "expr": "!currentValue" }, { "confirm": "Delete this task?", "expr": "deleteTask({ id: scopes.task.id })" } ] } }Answering No skips the confirmed step and every step after it. Any step can have
confirm, with or withoutset. This delete has nosetbecause it runs only for its effect.
List fields
An entry with each is a list: its component renders once per item.
There is no list container; the parent that names the list supplies it:
{"key":"task-table","component":"Table","children":["task-body"]}
{"key":"task-body","component":"TableBody","children":["task-row"]}
{"key":"task-row","component":"TableRow","each":"scopes.root.tasks","as":"task","keyBy":"id","children":["task-num","task-title"]}
{"key":"task-num","component":"TableCell","props":{"expr":"({ text: scopes.$task.index + 1 })"}}
{"key":"task-title","component":"TableCell","props":{"expr":"({ text: scopes.task.title })"}}Three tasks give one <tbody> with three <tr>s. The list’s children repeat
with it, each copy reading its own scopes.task and scopes.$task.
each
A bare expression returning the array to repeat over. Reactive: a write to a path it reads re-runs the list.
{ "each": "scopes.root.orders" } // a state array
{ "each": "scopes.root.orders.filter(o => o.status === 'open')" } // derived — still re-runs on every write to ordersLists nest, one entry per level. A list inside another list repeats for each
outer item, and its each can read that item:
{"key":"order-card","component":"Card","each":"scopes.root.orders","as":"order","keyBy":"id","props":{"expr":"({ title: 'Order ' + scopes.order.id })"},"children":["line-row"]}
{"key":"line-row","component":"Typography","each":"scopes.order.lines","as":"line","keyBy":"sku","props":{"expr":"({ text: scopes.line.qty + ' × ' + scopes.line.sku })"}}Each card lists the lines of its own order.
An each that evaluates to undefined renders zero rows, not an error.
as
The name of each item’s scopes, on the list entry and its whole subtree.
scopes.<as> is the item itself. scopes.$<as> is the row: index, id
(from keyBy), value for a primitive item, and the row’s
own state. So an as cannot start with $. See
State & Scopes.
as must be a new scope name: not root, and not the as of a list this
one sits inside. A reused name fails as invalid-list in the list’s
error slot:
{ "key": "order-row", "each": "scopes.root.orders", "as": "row", "children": ["line-item"] }
{ "key": "line-item", "each": "scopes.row.lines", "as": "row" }
// ❌ invalid-list: inside order-row, scopes.row already existskeyBy
Where the row’s id, the stable identity per item, comes from:
{ "each": "scopes.root.tasks", "as": "task", "keyBy": "id" } // { id: 7 } → 7; an item with no `id` falls back to its index
{ "each": "scopes.root.tasks", "as": "task" } // no keyBy → ids 0, 1, 2 …Give object items a stable key. Without one, the id is the index, so an
insert or a reorder moves row state to another item.
Partial replacement
Re-emitting an entry whose key exists replaces it and its subtree in
place. It is the only patch verb, used for mid-stream corrections and
edit turns. The new entry’s children
decide what survives. Already streamed:
{"key":"task-card","component":"Card","children":["task-rows","refresh"]}
{"key":"task-rows","component":"Typography","each":"scopes.root.tasks","as":"task","keyBy":"id","props":{"expr":"({ text: scopes.task.title })"}}
{"key":"refresh","component":"Button","props":{"literal":{"text":"Refresh"}}}Later in the same stream:
{"key":"task-card","component":"Card","children":["refresh","task-rows","empty-note"]}
{"key":"empty-note","component":"Typography","props":{"literal":{"text":"No tasks yet"}}}refresh and task-rows are not re-emitted, but the new children name them,
so they survive with their subtrees, reordered. A key left out of children is
dropped: omission is the delete.
State seeded above the replaced subtree is untouched, and the re-emitted
entry’s seed does not re-run, so state the user changed survives. Use this to
fix or restructure emitted UI; ongoing updates go through set.