Every part of a Mini App, in tables. For what these things are, read Mini Apps; for how to put them together, read Building Mini Apps.
All of it is defined in packages/app-runtime, shared by the browser, the
nodetool app debug harness, and the test suites — so all three behave the same.
Widgets
A widget is one thing on the app screen. Every widget except the layout widgets
can be wired to a value (binding), shown only under a condition
(visibleWhen), and greyed out under a condition (disabledWhen), on top of its
own settings. format is offered on the widgets that show text: Heading, Text,
Markdown, JSON, Output, Alert, Code, List, and Stat. The widget catalog
(WIDGET_CATALOG in packages/app-runtime/src/widgets.ts) is the single list the
editor, the debug harness, and the assistant all read. The editor palette groups
widgets as Inputs, Chat & AI, Actions, Display, and Layout.
Widgets that show something
| Widget | Shows |
|---|---|
| Heading | Fixed text at H1–H3. |
| Text | Fixed or formatted text. |
| Markdown | Rendered Markdown. The right choice for streamed prose. |
| Image | An image. Fit contain or cover, fixed height, placeholder, optional download button and filename. |
| Audio | An audio file, with a player, placeholder, optional download button and filename. |
| Video | A video, with a player, max height, placeholder, optional download button and filename. |
| Sketch | A sketch document, layers composited. Max height, optional canvas size. |
| Timeline | A timeline sequence, with its tracks and clips. Max height, optional metadata. |
| JSON | Structured data, formatted. |
| Table | A list, as rows. Max height, placeholder. |
| List | A list as bullets, or numbered with Numbered. Placeholder. |
| Key/Value | A record as label and value pairs. Placeholder. |
| Output | A value whose type varies; picks a display based on what arrives. Optional media download. |
| Progress | How far along the run is. |
| Agent Activity | What an agent did during the run: its text and each tool call with the outcome, live. Bind it to op:<opId>/exec#transcript. Label, height, placeholder. |
| Stat | One number with a label and a caption, for reading at a glance. Placeholder. |
| Alert | A message box. Severity info, success, warning, or error, optional title and text. Pair it with visibleWhen to show an error only when there is one. |
| Code | Text in a code block. Language label, max height. |
| Download | A button that saves the bound file. Label, file name, placeholder. |
| 3D Model | A 3D model viewer. Height, placeholder. |
| Chart | Plots a bound dataframe, list of records, or list of numbers. Chart type line, bar, scatter, or pie, height, placeholder. |
| A PDF viewer. Height, placeholder. | |
| Gallery | A list of images as tiles. Tile size, placeholder. Set selectionBinding and a tap picks one. |
| Image Compare | Two images under one wipe handle. binding is before, compareBinding after. Max height, placeholder. |
Sketch and Timeline take a document reference — {type: "sketch", id} or
{type: "timeline", id} — which is what the nodes that produce them emit. They
also accept the document inline, so a node that returns the payload rather than
a saved id renders too. Binding one to Image or Video instead shows nothing: a
reference is not a media URL. On mobile a Sketch draws the composited image and
falls back to a size and layer summary when it cannot load it, and a Timeline
shows a summary of duration, tracks, and clips rather than the sequence.
Widgets the user changes
“Commits” means the control reports a final value when you let go or click away. That’s what makes on-release pacing possible — see Triggers and pacing.
| Widget | Takes | Commits |
|---|---|---|
| Workflow Form | Every input of one operation at once, each rendered to match. | no |
| Workflow Input | Whatever the bound Input node declares, rendered to match. | no |
| Text Input | Text. Single-line or multiline. | yes |
| Number Input | A number. Min, max, step. | yes |
| Slider | A number. Min, max, step. | yes |
| Switch | On or off. | no |
| Select | One option from a fixed list. | no |
| Radio Group | One option from a fixed list, stacked or inline. | no |
| Checkbox Group | Any number of options from a fixed list, written as a list. Stacked or inline. | no |
| Choice Cards | One option shown as a card with title, description, and image. Options are authored, or read from a bound output through optionsBinding. Columns. |
no |
| Guided Steps | One step of an ordered list, as buttons. Writes the step’s value, starts on the first step, and allowBack can forbid going back. |
no |
| Date Input | A date, or a date and time with Include time. Writes text. | yes |
| Image Input | An image. | no |
| Sketch Pad | A drawing the user makes on the spot. Canvas size, white or transparent paper. | yes |
| Audio Input | An audio file. | no |
| Audio Recorder | Audio recorded from the microphone, then and there. | no |
| Video Input | A video. | no |
| Camera Capture | Video recorded from the camera, then and there. | no |
| Document Input | A document. | no |
| Color Input | A color. | no |
| Data Table Input | A table of rows. Max height. | no |
| File Path Input | A file path as text. | no |
| Folder Path Input | A folder path as text. | no |
| 3D Model Input | A 3D model. | no |
| Image Size Input | A width and height. | no |
| Media List Input | A list of images, videos, audio files, or text (List of). | no |
| Resource Picker | Picks which document a resource points at. | no |
| Resource Gallery | The same, from a grid of tiles. Tile size. | no |
| Storyboard Scenes | Edits the bound storyboard directly. Fires no event. | no |
Choice Cards and Guided Steps suit the pick-then-continue step of an approval flow. See recipe 6.
A Workflow Form is one widget where placing each input by hand would be several. It renders the inputs of the operation it names — the app’s only operation unless you pick another — so an Input node added to the workflow shows up in the app with no edit. Place the inputs one by one instead when the layout matters: a form renders them in graph order, top to bottom.
The Audio Recorder and Camera Capture write the same {type, uri, asset_id} ref
an upload writes, so bind them to an Audio or Video Input and the workflow reads
one value either way. The recording uploads as an asset when it finishes. On the
builder canvas they show a placeholder instead of asking for the microphone or
camera, so laying out an app never raises a permission prompt.
A Gallery with a selectionBinding is the generate-then-pick loop: bind the
gallery to a run’s list of images, the selection to the input of the next step,
and wire a run event to the selection so picking a tile starts it. What lands in
the binding is the item the run emitted, not the URL it rendered.
The Sketch Pad is the sketch editor’s canvas with a four-tool chrome — brush,
pencil, eraser, fill, plus colors, stroke size, undo and clear. Each finished
stroke flattens the drawing to a PNG and writes it as {type: "image", uri}, so
bind it to an Image Input and the workflow reads it the way it reads an upload.
White paper is the default because an image model is fed an opaque picture;
transparent keeps the alpha for a mask or an overlay. Nothing is written until
the user draws, so a pad wired to a run does not fire it on load. The drawing
travels inline as a data URI, so keep the canvas near its 512×384 default
rather than at the 2048px ceiling.
Chat and AI
| Widget | Does |
|---|---|
| Chat Thread | Shows a conversation. binding is the conversation (an app variable, or an output that emits messages), streamBinding the reply arriving from the current run. Max height, placeholder. |
| Chat Composer | Writes the next message and runs the operation. binding is the workflow input it sends to, historyBinding the conversation variable it appends to. Sends the message text, a message object, or the whole conversation. Optional image attachments. |
| Model Select | Picks a model and writes its reference. Kind: language, image, video, speech, transcription, or embedding. |
A conversation is a list of {role, content} objects. Anything else a binding
holds — a streamed string, a media ref, a list of results — reads as one
assistant turn, so a thread bound straight to an LLM output still renders.
The thread does the bookkeeping a second turn needs: when a run settles, it
folds the streamed reply into the conversation variable. An output slot is
cleared at the start of every run, so a reply left there alone would disappear
the moment the user sends again. This only happens when binding is a variable
and streamBinding is an output.
Buttons and layout
| Widget | Does |
|---|---|
| Button | Runs its click action. Style contained/outlined/text, color primary/secondary/warning. |
| Approval | Two buttons that write approved or rejected to its binding. Label, description, and button labels (defaults Approve and Needs changes). Fires a change event. |
| Panel | A titled box holding other widgets. |
| Columns | Two side-by-side slots, left and right. |
| Tabs | Up to three tabs, each a slot (tab1, tab2, tab3) with its own label. |
| Accordion | A titled section that opens and closes. defaultOpen sets its first state. |
| Spacer | Empty vertical space of a set height. |
| Divider | A horizontal line. |
Layout widgets carry no binding, condition, or format. To show or hide a group
under a condition, set visibleWhen on each widget inside it. Tabs and Accordion
let the person using the app hide a group themselves.
Bindings
A binding is the string that says what a widget is wired to. You usually pick these from a menu rather than typing them. They point at node ids, so renaming a node in the graph editor never breaks an app.
| Binding | Points at |
|---|---|
op:<opId>/in:<nodeId> |
An input of one of the app’s workflows. |
op:<opId>/out:<nodeId> |
An output of one of the app’s workflows. |
op:<opId>/prop:<nodeId>#<prop> |
A setting on a node, driven by a widget. |
op:<opId>/exec#<field> |
Run status: running, progress, error, activity, transcript. transcript is the agent text and tool calls of the latest run, in order. |
var:<variableId> |
A value the app remembers. |
view:<componentId>#<prop> |
State belonging to one widget. Never saved. |
node:<nodeId>#<prop> |
Old form of a node setting, resolved against the default operation. |
<name> |
Old form: a bare node name, looked up in the live graph. |
A bare name is looked up according to how the widget uses it: a widget the user
types into looks for an Input node, a widget that displays looks for an Output
node and then a variable, and a condition or format token tries outputs, then
inputs, then variables. A name that matches nothing is an error, not a silent
no-op.
Actions
A widget event runs one action. Actions are settings the runtime carries out — there’s nowhere to write code.
| Action | Settings | Effect |
|---|---|---|
run |
operationId |
Runs that workflow, following its concurrency rule. |
cancel |
operationId, optional invocationId |
Cancels that operation’s runs, or one specific run. |
setVariable |
variableId, and a value or the widget’s own value |
Writes a variable. |
toggleVariable |
variableId |
Flips an on/off variable. |
resourceCommand |
resourceBindingId, command |
read, create, update, delete, or upload on a resource. |
openResource |
resourceBindingId |
Opens that document in its editor. |
The visual editor offers Run workflow, Cancel run, Set variable, and Toggle variable. The resource actions are set through the agent or by editing the document.
Triggers and pacing
Events fire on click (buttons) or change (everything the user edits). A
change event also has a pace, which decides how often it fires while the user is
still editing:
| Pace | Fires |
|---|---|
live |
On every change. |
release |
Once, when the control settles — the slider is let go, the field loses focus. Only offered on controls that commit. |
debounce |
Once, after a short pause in editing. |
Conditions
visibleWhen and disabledWhen each hold a binding, an operator, and a value to
compare against. A condition whose binding points at nothing is treated as no
condition, so broken wiring never silently hides a widget.
| Operator | Editor label | True when |
|---|---|---|
notEmpty |
is not empty | The value is set, non-empty, and not false. |
empty |
is empty | The value is unset, empty, or false. |
eq |
equals | Equal, after converting your value to the same type. |
neq |
does not equal | Not equal. |
gt / gte |
is greater than / is at least | Numbers. A non-number never satisfies these. |
lt / lte |
is less than / is at most | Numbers. |
contains |
contains | Text contains the substring, or a list contains the item. |
You always type the comparison value as text, and it’s converted to match what
it’s compared against — so count gt "3" compares numbers and dark eq "true"
compares on/off.
Format templates
format replaces {binding} tokens with the value, optionally passed through
one filter: {op:main/out:n1|truncate:80}. An unknown filter or a binding that
points at nothing renders as nothing.
| Filter | Argument | Result |
|---|---|---|
number |
digits | The value as a number, optionally to N decimal places. |
date |
short |
Local date and time, or just the date with short. |
upper |
— | UPPERCASE. |
lower |
— | lowercase. |
join |
separator | A list joined into text; defaults to ", ". |
truncate |
length | Cut to N characters, with an ellipsis. |
Document schema
The rest of this page is the saved shape of an app — useful when editing a document by hand or reading one the agent wrote.
An app is stored as one ApplicationDocument. Ordinary apps use schema version 4.
Version 5 is for apps that carry a recipe manifest, and a
document with a higher version is rejected. Embedded version 1 and 2 documents
({ version, data } on a workflow) are upgraded on load, gaining one implicit
main operation bound to the workflow they came from.
interface ApplicationDocument {
schemaVersion: number; // 4, or 5 with a recipe
ui: PuckData; // the layout: { root, content, zones }
operations: OperationBinding[];
resources: ResourceBinding[];
variables: VariableDeclaration[];
theme?: { id: string }; // "default" | "centered" | "card"
recipe?: RecipeManifest;
}
Operations
One operation is one workflow the app can run, plus its wiring.
interface OperationBinding {
id: string;
name: string;
workflowId: string; // empty for a script operation
workflowVersion?: number; // fixed in a published app, latest in a draft
target?: // set when the operation runs a script
| { kind: "workflow"; workflowId: string; workflowVersion?: number }
| { kind: "script"; scriptId: string; scriptVersion: number };
inputs: Record<string, InputMapping>; // keyed by node id
outputs: Record<string, OutputMapping>; // keyed by node id
policy: "parallel" | "replace" | "queue";
timeoutMs?: number;
}
| Input mapping | Where the value comes from |
|---|---|
{ from: "widget" } |
The widget wired to it. The default when nothing else is set. |
{ from: "variable", variableId } |
A value the app remembers. |
{ from: "constant", value } |
A fixed value. |
{ from: "resource", resourceBindingId } |
Whichever document that resource currently points at. |
| Output mapping | Where the value goes |
|---|---|
{ to: "display" } |
The slot display widgets read. |
{ to: "variable", variableId } |
A variable, and the display slot. |
policy |
If you start a run while one is already going |
|---|---|
parallel |
Start anyway. |
replace |
Cancel the running one, then start. The default. |
queue |
Wait for it to finish, then start. |
Several operations may point at the same workflow with different wiring, and an app that runs three workflows declares three operations.
A script operation runs a saved JS script at a pinned version instead of a
workflow. The script’s declared input and output names stand in for node ids, so
inputs and outputs key on those names. Publishing checks that the pinned
version exists. A public link cannot serve a script operation.
Variables
interface VariableDeclaration {
id: string;
name: string;
type?: { type: string; optional?: boolean } | null;
default?: unknown;
scope: "instance" | "user"; // this open app, or saved per user
persist: boolean; // only user-scoped variables may be saved
}
The visual editor’s variable picker lists the Set Variable channels the graph publishes. The App Data panel in the Design header declares variables with a type, default, scope, and persistence. Operations and resources are edited there too, and the assistant edits the same lists.
Resources
A resource is a handle to a real document the app may read or edit.
interface ResourceBinding {
id: string;
name: string;
kind: "asset" | "timeline" | "storyboard" | "sketch";
scope: { projectId?: string; fixedId?: string };
operations: ("read" | "create" | "update" | "delete")[];
}
Recipe manifest
A recipe is optional metadata that marks an app as a recipe: named, typed
inputs, a fixed set of operations, and declared outputs. It holds a slug, typed
inputs (text, number, boolean, image, video, audio, color, asset,
entity, storyboard, timeline), the operations it uses with their intent,
outputs, and optional creative strategy, preservation rules, media policy,
presentation, and marketing fields. The document is rejected unless every recipe
input has an app variable with the same id and a compatible type, every required
input is mapped into a recipe operation, and every output binding resolves.
An ordinary update cannot strip the manifest from an app that has one.
Bundles
A bundle is an app packaged for sharing: the app plus the full graph of every workflow it runs, in one JSON file. Export, import, and the shipped example apps all use it.
interface ApplicationBundle {
schemaVersion: number; // 1
name: string;
description: string;
app: ApplicationDocument; // operations reference workflows[].key and scripts[].key
workflows: {
key: string; name: string; description?: string; graph: Graph;
sourceId?: string; // stable identity across installs
version?: number | null; graphHash?: string | null;
}[];
scripts: { key: string; name: string; document: JsScriptDocument;
sourceId?: string; version?: number | null }[];
}
Inside a bundle, each operation’s workflowId (or script id) holds a local
nickname (key) instead of a real id. Importing creates the workflows and swaps
the nicknames for the new ids, the same trick .nodetool workflow bundles use for
their asset references. A workflow with a sourceId gets a stable row id, so
importing two bundles that ship the same workflow reuses one row. A bundle
exported with --released carries the locked-in graphs, so it reproduces exactly
what that release ran.
What one open app holds
| Group | Keyed by | Holds |
|---|---|---|
inputs |
opId:nodeId, or opId:nodeId#prop |
{ value, dirty, revision }. dirty turns true once a widget rather than a default wrote it. |
outputs |
opId:nodeId |
{ value, invocationId, status, revision }. Status is empty, pending, streaming, or done. |
variables |
variable id | The current value. |
view |
componentId:prop |
State belonging to one widget. |
invocations |
job id | { id, operationId, status, progress, error, startedAt }. |
A run’s status is pending, running, completed, failed, or cancelled.
Streamed values accumulate: text is joined, structured items collect into a list.
A message from a job this app didn’t start is thrown away.
Agent tools
The builder agent edits the open app through these tools. Every one takes an
application_id — the app’s own id, listed in the ui_context block. A workflow
id is never accepted; the workflows an app runs are named by
target_workflow_id on its operations.
| Area | Tools |
|---|---|
| Layout | ui_app_get_snapshot, ui_app_list_component_types, ui_app_add_component, ui_app_update_component, ui_app_remove_component, ui_app_select_component, ui_app_set_title |
| Operations | ui_app_list_operations, ui_app_add_operation, ui_app_update_operation, ui_app_remove_operation |
| Variables | ui_app_list_variables, ui_app_declare_variable, ui_app_update_variable, ui_app_remove_variable |
| Resources | ui_app_list_resources, ui_app_add_resource, ui_app_remove_resource |
| Bindings | ui_app_get_binding_targets |
| Checking | ui_app_debug |
ui_app_debug sends the open draft, unsaved edits included, to the server and
returns the same verdict as nodetool app debug. With run false (the default)
it is a free wiring check. With run true it executes the workflows and spends
money, so run it once at the end. It also takes params, interact, and
timeout_ms.
REST API
The same service backs the web app’s tRPC calls and these routes, which mobile, the CLI, and other clients use.
| Route | Does |
|---|---|
GET /api/applications |
List apps. |
POST /api/applications |
Create an app, empty or from a workflow. |
GET, PUT, PATCH, DELETE /api/applications/:id |
Read, replace, update, or delete one app. |
GET /api/applications/:id/released-document |
The released snapshot and the graphs it pinned, or null. |
GET /api/applications/:id/export-bundle |
Download the app as a bundle. |
POST /api/applications/import-bundle |
Create an app and its workflows from a bundle. |
POST /api/applications/debug |
Run the debug harness on a document. |
POST /api/applications/build |
Build an app from a prompt. |
POST /api/applications/spec |
Write the spec a build would use, and stop. |
GET /api/applications/examples |
List the shipped example apps. |
GET /api/applications/examples/:slug |
One example as a bundle. |
POST /api/applications/examples/:slug/install |
Add an example to your apps. |
Publishing, versions, rollback, the budget, and the public link are tRPC
procedures on applications (publish, versions, release, setBudget,
deploy, and others). A public link is served at /a/<token>.
CLI
npm run dev:nodetool -- app debug <application_id>
npm run dev:nodetool -- app debug my-app.json --params '{"prompt":"hi"}'
npm run dev:nodetool -- app debug <id> --no-run # check the wiring, don't run
npm run dev:nodetool -- app debug <id> --json # full report
# Script the clicks and typing
npm run dev:nodetool -- app debug <id> --interact \
'[{"set":{"key":"prompt","value":"hi"}},{"click":"Button-1"}]'
npm run dev:nodetool -- app debug <id> --interact \
'[{"set":{"key":"tone","value":"terse","operationId":"draft"}},{"run":"draft"}]'
# Timeout, report folder, build, and bundles
npm run dev:nodetool -- app debug <id> --timeout 60000 --out ./app-report
npm run dev:nodetool -- app build "<prompt>" -p anthropic -m claude-sonnet-5
npm run dev:nodetool -- apps list
npm run dev:nodetool -- apps export-bundle <id> -o my.app.json --released
npm run dev:nodetool -- apps import-bundle my.app.json --project <project_id>
An interaction step is one of {"set":{"key","value","operationId"}},
{"change":"<widget>","value":…}, {"click":"<widget>"}, {"run":"<operationId>"},
{"cancel":"<operationId>"}, or {"seedResource":{"id","items"}}. --params sets
values before the steps run. The command exits 0 when the verdict is ok.
app build and the apps commands are covered in
the harness reference
and CLI.
The harness runs every operation the app declares, not just the first. Widgets
are clicked by component id, by type if only one exists, or by label if it’s
unique. Results land in nodetool-debug/app-<id>-<ts>/ (or the --out folder) as report.json,
report.md, app.json, workflow.json, and one
server/run-N.messages.jsonl per run.
The harness evaluates visibleWhen, disabledWhen, and format for scripted
interactions. It does not render layout, styling, focus, or scroll, and it does
not read the user’s resource library. Resource tests must seed an in-memory
collection with seedResource or a resource:<binding-id> parameter. Reactive
browser subgraph runs are reported separately because the harness executes the
whole workflow.
Related
- Mini Apps — concepts and runtime
- Building Mini Apps — recipes per use case
- App Builder — the editor
- CLI — the full command reference