> ## Documentation Index
> Fetch the complete documentation index at: https://agentheya.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Generative UI

> Let your agent render cards, tables, metrics, alerts, and interactive forms directly in the chat — and collect structured input back from the user.

Agentheya agents can go beyond plain text and generate rich visual interfaces during a conversation. Instead of describing data in words, the agent can show a formatted card, a metrics row, a status table, or an interactive form — and, when the form is interactive, read the user's answers back on its next turn.

This page is the complete reference: every component, every prop, how to make the UI dynamic, and a set of worked examples for building real, multi-field forms.

<Note>
  All visual components follow the [json-render](https://json-render.dev) spec, but Agentheya ships its own renderer with a **focused, well-supported subset**. Stick to what's documented here — see [Limitations](#limitations) for the features that exist in the underlying spec but are **not** rendered by Agentheya yet.
</Note>

## Where Generative UI shows up

There are three ways a spec reaches the screen. They all use the **same component spec** and the **same renderer**, so everything on this page applies to all three.

<CardGroup cols={3}>
  <Card title="render_ui tool" icon="wand-magic-sparkles">
    A [built-in tool](/builtin-tools/render_ui) the agent calls to draw UI on demand. Can be static or interactive. Enable it on the **Tools** tab.
  </Card>

  <Card title="Custom tool response" icon="screwdriver-wrench">
    Your own API/code [tool](/sidebar-menu/tools) returns a `_render` payload and the agent displays it. Can be interactive.
  </Card>

  <Card title="Inline artifact" icon="align-left">
    The agent embeds a **read-only** component inside its text reply when a visual is clearer than prose. Always static.
  </Card>
</CardGroup>

| Surface          | Where the spec goes                                                    | Interactive?   |
| ---------------- | ---------------------------------------------------------------------- | -------------- |
| `render_ui` tool | under the tool's `spec` argument (`interactive: true` for input)       | Yes (optional) |
| Custom tool      | under a `_render` key in the tool's JSON result (`_interactive: true`) | Yes (optional) |
| Inline artifact  | emitted automatically by the agent                                     | No (read-only) |

## The spec model

A spec is a **flat map of elements** plus a pointer to the root and an optional state object:

```json theme={null}
{
  "root": "panel",
  "elements": {
    "panel": { "type": "card", "props": { "title": "Hello", "subtitle": null }, "children": ["msg"] },
    "msg":   { "type": "alert", "props": { "title": null, "message": "It works.", "variant": "info" }, "children": [] }
  },
  "state": {}
}
```

* **`root`** — the id (a key in `elements`) of the top-level element to render.
* **`elements`** — a map of `id → element`. Elements are **not** nested; nesting is expressed by listing child ids in `children`.
* **`state`** — the data model. Seed it with the starting values for any input fields (see [Making it dynamic](#making-it-dynamic)).

Every element has the same shape:

```json theme={null}
{
  "type": "card",
  "props": { "...": "..." },
  "children": ["childId1", "childId2"],
  "visible": { "$state": "/show", "eq": true },
  "on": { "press": { "action": "submit" } }
}
```

| Field      | Required | Purpose                                                                                 |
| ---------- | -------- | --------------------------------------------------------------------------------------- |
| `type`     | yes      | One of the [components](#component-reference) below.                                    |
| `props`    | yes      | The component's properties. **Every prop key must be present** — see the warning below. |
| `children` | yes      | Array of child element ids. Use `[]` for leaf components.                               |
| `visible`  | no       | A [condition](#conditional-visibility) controlling whether the element renders.         |
| `on`       | no       | Event → [action](#interactivity-and-submission) bindings (e.g. a button's `press`).     |

<Warning>
  **Every prop in the schema must be present**, even when you don't need it — set unused props to `null`. Validation rejects a spec with a *missing* key (`null` is fine, *absent* is not). For example a `card` must include both `title` **and** `subtitle`; if there's no subtitle, write `"subtitle": null`. This is the single most common cause of a rejected spec.
</Warning>

## Component reference

Eleven components are available. Each table lists **all** props (remember: all are required-present; use `null` for unused ones). Allowed enum values are shown in `code`.

### Containers

These hold other elements via `children`.

#### card

A titled panel.

| Prop       | Type           | Notes                        |
| ---------- | -------------- | ---------------------------- |
| `title`    | string \| null | Heading.                     |
| `subtitle` | string \| null | Sub-heading under the title. |

```json theme={null}
{ "type": "card", "props": { "title": "Order #4821", "subtitle": null }, "children": ["body"] }
```

#### list

A flex container that lays its children in a row or column.

| Prop        | Type                               | Notes                     |
| ----------- | ---------------------------------- | ------------------------- |
| `direction` | `vertical` \| `horizontal` \| null | Defaults to vertical.     |
| `gap`       | `sm` \| `md` \| `lg` \| null       | Spacing between children. |

```json theme={null}
{ "type": "list", "props": { "direction": "horizontal", "gap": "lg" }, "children": ["a", "b", "c"] }
```

#### form

Groups inputs and a submit button. Visually a labelled section; submission still happens via a `button` wired to the `submit` action (see [Interactivity](#interactivity-and-submission)).

| Prop          | Type           | Notes                        |
| ------------- | -------------- | ---------------------------- |
| `title`       | string \| null | Section heading.             |
| `description` | string \| null | Helper text under the title. |

```json theme={null}
{ "type": "form", "props": { "title": "Contact details", "description": null }, "children": ["nameInput", "submitBtn"] }
```

#### divider

A horizontal rule. No props.

```json theme={null}
{ "type": "divider", "props": {}, "children": [] }
```

### Display

Read-only components for showing data. All are leaf elements (`children: []`).

#### stat

A single KPI: label, value, and an optional trend.

| Prop         | Type                                | Notes                      |
| ------------ | ----------------------------------- | -------------------------- |
| `label`      | string                              | Metric name.               |
| `value`      | string \| number                    | The figure.                |
| `change`     | string \| null                      | Trend text, e.g. `"+12%"`. |
| `changeType` | `up` \| `down` \| `neutral` \| null | Colours/arrows the trend.  |
| `prefix`     | string \| null                      | e.g. `"$"`.                |
| `suffix`     | string \| null                      | e.g. `"/mo"`.              |

```json theme={null}
{ "type": "stat", "props": { "label": "Revenue", "value": "12,450", "change": "+8%", "changeType": "up", "prefix": "$", "suffix": null }, "children": [] }
```

<Warning>
  `changeType` is `up` / `down` / `neutral` — **not** `increase` / `decrease`. The wrong value fails validation.
</Warning>

#### badge

A small inline label / chip.

| Prop      | Type                                                              | Notes      |
| --------- | ----------------------------------------------------------------- | ---------- |
| `label`   | string                                                            | Chip text. |
| `variant` | `default` \| `info` \| `success` \| `warning` \| `danger` \| null | Colour.    |

```json theme={null}
{ "type": "badge", "props": { "label": "Connected", "variant": "success" }, "children": [] }
```

#### alert

A banner notice. Use `variant` for severity.

| Prop      | Type                                                              | Notes                                                  |
| --------- | ----------------------------------------------------------------- | ------------------------------------------------------ |
| `title`   | string \| null                                                    | Optional bold line.                                    |
| `message` | string                                                            | The body text. **The field is `message`, not `body`.** |
| `variant` | `default` \| `info` \| `success` \| `warning` \| `danger` \| null | Colour.                                                |

```json theme={null}
{ "type": "alert", "props": { "title": "Heads up", "message": "Your trial ends in 2 days.", "variant": "warning" }, "children": [] }
```

#### key\_value

A definition list of label/value pairs.

| Prop    | Type                      | Notes                                                          |
| ------- | ------------------------- | -------------------------------------------------------------- |
| `items` | array of `{ key, value }` | `value` may be a string, number, boolean, or null. 1–50 items. |

```json theme={null}
{
  "type": "key_value",
  "props": { "items": [
    { "key": "Customer", "value": "Jane Smith" },
    { "key": "Amount",   "value": "$149.00" },
    { "key": "Paid",     "value": true }
  ] },
  "children": []
}
```

#### table

A simple data table.

| Prop      | Type             | Notes                                                            |
| --------- | ---------------- | ---------------------------------------------------------------- |
| `caption` | string \| null   | Optional caption.                                                |
| `columns` | array of strings | Header row. 1–20 columns.                                        |
| `rows`    | array of arrays  | Each cell is a string, number, boolean, or null. Up to 200 rows. |

```json theme={null}
{
  "type": "table",
  "props": {
    "caption": "Top issues",
    "columns": ["Issue", "Count", "Resolved"],
    "rows": [
      ["Billing", 87, 81],
      ["Password reset", 65, 65],
      ["Feature requests", 44, 12]
    ]
  },
  "children": []
}
```

### Interactive

These collect input or trigger submission. They only do anything when the render is **interactive** (the `render_ui` tool sets `interactive: true`, or a custom tool sets `_interactive: true`).

#### button

A clickable button. Bind its behaviour with `on.press`.

| Prop       | Type                                                    | Notes         |
| ---------- | ------------------------------------------------------- | ------------- |
| `label`    | string                                                  | Button text.  |
| `variant`  | `primary` \| `secondary` \| `danger` \| `ghost` \| null | Style.        |
| `disabled` | boolean \| null                                         | Greys it out. |

```json theme={null}
{
  "type": "button",
  "props": { "label": "Submit", "variant": "primary", "disabled": null },
  "on": { "press": { "action": "submit" } },
  "children": []
}
```

#### input

A form field. The `kind` prop selects the variant; `name` is how the value is identified. **All nine props are required-present** — use `null` for the ones a given kind doesn't use.

| Prop          | Type                                           | Notes                                                    |
| ------------- | ---------------------------------------------- | -------------------------------------------------------- |
| `kind`        | see below                                      | Field variant.                                           |
| `name`        | string                                         | Identifier, `^[a-z][a-z0-9_]{0,40}$` (case-insensitive). |
| `label`       | string \| null                                 | Field label.                                             |
| `placeholder` | string \| null                                 | Placeholder text.                                        |
| `required`    | boolean \| null                                | Marks the field required.                                |
| `value`       | literal \| `{ "$bindState": "/path" }` \| null | Current value / two-way binding.                         |
| `options`     | array of `{ value, label }` \| null            | **Required for `select`.**                               |
| `min`         | number \| null                                 | For numeric kinds.                                       |
| `max`         | number \| null                                 | For numeric kinds.                                       |

**`kind` values:** `text`, `email`, `number`, `integer`, `tel`, `url`, `password`, `textarea`, `select`, `checkbox`.

```json theme={null}
{
  "type": "input",
  "props": {
    "kind": "select",
    "name": "priority",
    "label": "Priority",
    "placeholder": null,
    "required": true,
    "value": { "$bindState": "/priority" },
    "options": [
      { "value": "low",  "label": "Low" },
      { "value": "high", "label": "High" }
    ],
    "min": null,
    "max": null
  },
  "children": []
}
```

<Note>
  Field rules worth memorising:

  * Use **`kind`** (not `type`) to choose the input variant.
  * `select` `options` must be **objects** `{ "value": "...", "label": "..." }` — not plain strings.
  * Every input needs a **`name`**.
  * An input with no `value: { "$bindState": "/path" }` shows a ⚠ marker and **its value is not collected on submit**. Bind every field you want back.
  * `number`/`integer` return a number (or `null` when empty); `checkbox` returns a boolean; everything else returns a string.
  * There is **no** `date`, `radio`, or `multiselect` kind. Use `text` for dates and `select` for single choice. (Multi-select can be modelled as several `checkbox` fields.)
</Note>

## Making it dynamic

The **state** object is the single source of truth. Inputs read from it and write back to it as the user types; display props read from it; and you reference it from visibility conditions. This is how a form reacts live to what the user does — no agent round-trip needed.

### Expressions

Any **value-bearing prop** (titles, labels, `stat.value`, `alert.message`, `badge.variant`, …) may be a literal **or** one of these expression objects. Seed each referenced path in `state` with a starting value (`""`, `0`, `false`, …). Paths are [JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901) (`/contact/email`, not `contact.email`).

| Expression                                             | Meaning                                                                                                                                       |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `{ "$state": "/path" }`                                | Read the value at `/path` (read-only).                                                                                                        |
| `{ "$bindState": "/path" }`                            | **Two-way** binding — use on an input's `value` to show the current value *and* write edits back. Put this on every field you want collected. |
| `{ "$template": "Total: ${/cart/total}" }`             | Interpolate state values into a string by absolute pointer.                                                                                   |
| `{ "$cond": <condition>, "$then": <v>, "$else": <v> }` | Pick a value by condition (same condition syntax as [visibility](#conditional-visibility)) — e.g. drive a `variant` colour off a state flag.  |

<Note>
  Structural props stay literal: input `kind`/`name`/`options`, `table.columns`/`rows`, and `key_value.items` are not expression slots. An object with no recognised `$`-key is rejected, so typos still fail validation.
</Note>

### Conditional visibility

Any element can carry a top-level `visible` condition. When it evaluates false, the element (and its children) is hidden. Combined with `$bindState` inputs, this is the core technique for forms that reveal fields based on earlier answers.

| Form                                 | Visible when…                          |
| ------------------------------------ | -------------------------------------- |
| `{ "$state": "/x" }`                 | `/x` is truthy                         |
| `{ "$state": "/x", "not": true }`    | `/x` is falsy                          |
| `{ "$state": "/x", "eq": "value" }`  | equals `value`                         |
| `{ "$state": "/x", "neq": "value" }` | not equal                              |
| `{ "$state": "/n", "gt": 5 }`        | greater than (also `gte`, `lt`, `lte`) |
| `{ "$and": [cond, cond] }`           | all true                               |
| `{ "$or": [cond, cond] }`            | any true                               |
| `[cond, cond]`                       | all true (shorthand for `$and`)        |

Add `"not": true` to any single condition to invert it. Use **one** comparison operator per condition object.

### Repeating over a list

An element can carry a top-level `repeat` to render its children **once per item** in a state array. The element itself renders once as the container; its children expand per item.

```json theme={null}
"repeat": { "statePath": "/items", "key": "id" }
```

Inside the repeated children, reference the current item:

| Expression                 | Resolves to                                                                    |
| -------------------------- | ------------------------------------------------------------------------------ |
| `{ "$item": "field" }`     | a field on the current item (e.g. its `name`)                                  |
| `{ "$item": "" }`          | the whole current item                                                         |
| `{ "$index": true }`       | the current array index (a number)                                             |
| `{ "$bindItem": "field" }` | **two-way** binding to a field on the current item (use on an input's `value`) |

`key` (optional) names a stable id field on each item. Pair `repeat` with the `pushState` / `removeState` actions below to build add/remove lists ([Recipe 7](#recipe-7-dynamic-line-items)).

## Interactivity and submission

A render is interactive when the surface enables it (`render_ui` with `interactive: true`, or a custom tool with `_interactive: true`). Interactive specs must include at least one `button` wired to an action.

Actions are bound on `on.press` (buttons) and fire in order. Three kinds:

* **Local** — mutate `state` in place, form stays open (`setState`, `pushState`, `removeState`).
* **Server** — run a sandboxed code snippet or invoke a tool when clicked, surface the result inline / in state / in an overlay. Form stays open so the user can keep interacting (`runCode`, `callTool`).
* **Terminal** — end the interaction and report back to the agent (`submit`, `cancel`).

| Action        | Kind     | Effect                                                                                                                                                                     |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submit`      | terminal | Collects the **entire state object** and sends it to the agent. Add `params.intent` to label which button was pressed.                                                     |
| `cancel`      | terminal | Dismisses the interaction; the agent receives `status: "cancelled"` with no values.                                                                                        |
| `setState`    | local    | Set `params.statePath` to `params.value`.                                                                                                                                  |
| `pushState`   | local    | Append `params.value` to the array at `params.statePath`. Use `"$id"` anywhere in the value to generate a unique id; `params.clearStatePath` resets an input after adding. |
| `removeState` | local    | Remove item `params.index` from the array at `params.statePath` (use `{ "$index": true }` inside a repeat).                                                                |
| `runCode`     | server   | Run sandboxed code (Python / JavaScript) and surface the result. See [Server actions](#server-actions) below.                                                              |
| `callTool`    | server   | Invoke a named tool with merged static + form params. See [Server actions](#server-actions) below.                                                                         |

```json theme={null}
"on": { "press": { "action": "submit", "params": { "intent": "approve" } } }
```

### Server actions

`runCode` and `callTool` let a button trigger work on the server without ending the interaction — the form stays open, the result is delivered back to the spec, and the user can keep clicking. Both actions require the render to be **interactive** (`render_ui` with `interactive: true`, or `_interactive: true` on a custom tool); a static render with these verbs is rejected.

Both verbs share the same `resultMode` / `target` contract:

| `resultMode` | Where the result goes                                                                         | `target`                                        |
| ------------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `inline`     | Written into state at `target`; bind any element to `{ "$state": "<target>" }` to display it. | JSON Pointer, e.g. `"/searchResult"`. Required. |
| `variable`   | Same as `inline` — kept as a separate verb so future updates can differentiate.               | JSON Pointer. Required.                         |
| `modal`      | Shown in a stock modal overlay.                                                               | Ignored.                                        |
| `toast`      | Shown as a transient toast.                                                                   | Ignored.                                        |

The current form `state` is sent with every click, so the code / tool sees up-to-date input values.

#### `runCode` — sandboxed snippet

```json theme={null}
{
  "action": "runCode",
  "params": {
    "code": "def handle(params, ctx):\n    return {'total': params['qty'] * params['price']}",
    "lang": "python",
    "resultMode": "inline",
    "target": "/lineTotal"
  }
}
```

`params.code` is the source. `params.lang` is `"python"` (default) or `"javascript"`. The runtime, sandbox isolation, and limits match those of the [`execute_code`](/sidebar-menu/tools#code-tools) tool.

#### `callTool` — invoke a registered tool

```json theme={null}
{
  "action": "callTool",
  "params": {
    "tool": "web_search",
    "params": { "max_results": 5 },
    "resultMode": "inline",
    "target": "/results"
  }
}
```

`params.tool` is the tool name (anything already available to this agent). `params.params` is a static argument object — the **form state is merged on top** at click time, so dynamic fields like `query` flow through naturally. The result is whatever the tool returns.

Example button:

```json theme={null}
"searchBtn": {
  "type": "button",
  "props": { "label": "Search", "variant": "primary" },
  "on": { "press": { "action": "callTool", "params": {
    "tool": "web_search",
    "params": { "max_results": 5 },
    "resultMode": "inline",
    "target": "/results"
  } } }
}
```

A related **`watch`** field (top-level on an element) fires a local action when a state path changes — handy for resetting a dependent field when a parent select changes:

```json theme={null}
"watch": { "/country": { "action": "setState", "params": { "statePath": "/city", "value": "" } } }
```

### What the agent receives

After the user acts, the agent's **next turn** gets:

```json theme={null}
{
  "status": "submitted",
  "result": {
    "values": { "name": "Jane", "priority": "high" },
    "action": "submit",
    "params": { "intent": "approve" }
  }
}
```

`values` is the **whole state model** — every `$bindState` path becomes a key. (Cancellation arrives as `{ "status": "cancelled" }` with no `values`.)

<Steps>
  <Step title="Agent renders an interactive form">
    It calls `render_ui` with `interactive: true` (or your tool returns `_render` + `_interactive: true`) and then ends its turn.
  </Step>

  <Step title="User fills it in and clicks a button">
    Inputs write to `state` as they type; the `submit`/`cancel` action fires on click.
  </Step>

  <Step title="Agent reads the result next turn">
    The submitted `values` / `action` / `params` arrive as the tool result, and the agent continues.
  </Step>
</Steps>

<Warning>
  After an interactive render, the agent **must end its turn and wait** — it should not narrate the outcome until the submission actually arrives. For destructive actions, gate the follow-through with [`confirm_action`](/builtin-tools/confirm_action) + [`commit_action`](/builtin-tools/commit_action); `render_ui` does not provide that guarantee on its own.
</Warning>

## Form recipes

Each recipe below is a complete, valid `spec` (the `{ root, elements, state }` object). For the `render_ui` tool, pass it as the `spec` argument with `interactive: true`; for a custom tool, return it under `_render` with `_interactive: true`.

### Recipe 1 — Multi-section intake form

Sections via nested cards, a mix of input kinds, one submit button. Every bound field comes back in `values`.

```json theme={null}
{
  "root": "form",
  "elements": {
    "form": {
      "type": "card",
      "props": { "title": "New customer", "subtitle": "All fields with * are required" },
      "children": ["personal", "divider", "account", "submitBtn"]
    },
    "personal": {
      "type": "card",
      "props": { "title": "Personal", "subtitle": null },
      "children": ["fullName", "email", "phone"]
    },
    "fullName": {
      "type": "input",
      "props": { "kind": "text", "name": "full_name", "label": "Full name *", "placeholder": "Jane Smith", "required": true, "value": { "$bindState": "/full_name" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "email": {
      "type": "input",
      "props": { "kind": "email", "name": "email", "label": "Email *", "placeholder": "jane@example.com", "required": true, "value": { "$bindState": "/email" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "phone": {
      "type": "input",
      "props": { "kind": "tel", "name": "phone", "label": "Phone", "placeholder": null, "required": false, "value": { "$bindState": "/phone" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "divider": { "type": "divider", "props": {}, "children": [] },
    "account": {
      "type": "card",
      "props": { "title": "Account", "subtitle": null },
      "children": ["plan", "seats", "newsletter"]
    },
    "plan": {
      "type": "input",
      "props": { "kind": "select", "name": "plan", "label": "Plan *", "placeholder": null, "required": true, "value": { "$bindState": "/plan" }, "options": [ { "value": "solo", "label": "Solo" }, { "value": "pro", "label": "Pro" }, { "value": "team", "label": "Team" } ], "min": null, "max": null },
      "children": []
    },
    "seats": {
      "type": "input",
      "props": { "kind": "integer", "name": "seats", "label": "Seats", "placeholder": null, "required": false, "value": { "$bindState": "/seats" }, "options": null, "min": 1, "max": 500 },
      "children": []
    },
    "newsletter": {
      "type": "input",
      "props": { "kind": "checkbox", "name": "newsletter", "label": "Subscribe to product updates", "placeholder": null, "required": false, "value": { "$bindState": "/newsletter" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "submitBtn": {
      "type": "button",
      "props": { "label": "Create customer", "variant": "primary", "disabled": null },
      "on": { "press": { "action": "submit" } },
      "children": []
    }
  },
  "state": { "full_name": "", "email": "", "phone": "", "plan": "pro", "seats": 1, "newsletter": false }
}
```

### Recipe 2 — Conditional fields (reveal on selection)

The contact-method `select` drives which follow-up field is visible, using `visible` + `$bindState`. No scripting required — picking an option re-renders the dependent fields.

```json theme={null}
{
  "root": "card",
  "elements": {
    "card": {
      "type": "card",
      "props": { "title": "How should we reach you?", "subtitle": null },
      "children": ["method", "emailField", "phoneField", "otherNote", "submitBtn"]
    },
    "method": {
      "type": "input",
      "props": { "kind": "select", "name": "contact_method", "label": "Preferred channel", "placeholder": null, "required": true, "value": { "$bindState": "/contact_method" }, "options": [ { "value": "email", "label": "Email" }, { "value": "phone", "label": "Phone" }, { "value": "other", "label": "Something else" } ], "min": null, "max": null },
      "children": []
    },
    "emailField": {
      "type": "input",
      "visible": { "$state": "/contact_method", "eq": "email" },
      "props": { "kind": "email", "name": "email", "label": "Email address", "placeholder": "jane@example.com", "required": true, "value": { "$bindState": "/email" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "phoneField": {
      "type": "input",
      "visible": { "$state": "/contact_method", "eq": "phone" },
      "props": { "kind": "tel", "name": "phone", "label": "Phone number", "placeholder": null, "required": true, "value": { "$bindState": "/phone" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "otherNote": {
      "type": "input",
      "visible": { "$state": "/contact_method", "eq": "other" },
      "props": { "kind": "textarea", "name": "other_note", "label": "Tell us how", "placeholder": "e.g. WhatsApp, Telegram…", "required": true, "value": { "$bindState": "/other_note" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "submitBtn": {
      "type": "button",
      "props": { "label": "Continue", "variant": "primary", "disabled": null },
      "on": { "press": { "action": "submit" } },
      "children": []
    }
  },
  "state": { "contact_method": "", "email": "", "phone": "", "other_note": "" }
}
```

### Recipe 3 — Reveal a whole section

A `visible` condition on a **container** shows or hides all of its children at once. Ticking "ship to a different address" reveals an entire address sub-form. (Validation accepts `visible` on any element — it's an element-level field, not a prop.)

```json theme={null}
{
  "root": "checkout",
  "elements": {
    "checkout": {
      "type": "card",
      "props": { "title": "Checkout", "subtitle": null },
      "children": ["differentAddr", "shipping", "submitBtn"]
    },
    "differentAddr": {
      "type": "input",
      "props": { "kind": "checkbox", "name": "different_address", "label": "Ship to a different address", "placeholder": null, "required": false, "value": { "$bindState": "/different_address" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "shipping": {
      "type": "card",
      "visible": { "$state": "/different_address", "eq": true },
      "props": { "title": "Shipping address", "subtitle": null },
      "children": ["shipName", "shipLine1", "shipCity", "shipZip"]
    },
    "shipName": {
      "type": "input",
      "props": { "kind": "text", "name": "ship_name", "label": "Recipient", "placeholder": null, "required": true, "value": { "$bindState": "/ship_name" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "shipLine1": {
      "type": "input",
      "props": { "kind": "text", "name": "ship_line1", "label": "Address", "placeholder": null, "required": true, "value": { "$bindState": "/ship_line1" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "shipCity": {
      "type": "input",
      "props": { "kind": "text", "name": "ship_city", "label": "City", "placeholder": null, "required": true, "value": { "$bindState": "/ship_city" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "shipZip": {
      "type": "input",
      "props": { "kind": "text", "name": "ship_zip", "label": "Postcode", "placeholder": null, "required": true, "value": { "$bindState": "/ship_zip" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "submitBtn": {
      "type": "button",
      "props": { "label": "Place order", "variant": "primary", "disabled": null },
      "on": { "press": { "action": "submit" } },
      "children": []
    }
  },
  "state": { "different_address": false, "ship_name": "", "ship_line1": "", "ship_city": "", "ship_zip": "" }
}
```

### Recipe 4 — Decision card with two buttons (`intent`)

Show details, then offer two outcomes. The agent tells them apart via `params.intent` in the result.

```json theme={null}
{
  "root": "card",
  "elements": {
    "card": {
      "type": "card",
      "props": { "title": "Refund request #5512", "subtitle": null },
      "children": ["details", "note", "divider", "actions"]
    },
    "details": {
      "type": "key_value",
      "props": { "items": [
        { "key": "Customer", "value": "Acme Co." },
        { "key": "Amount",   "value": "$420.00" },
        { "key": "Reason",   "value": "Duplicate charge" }
      ] },
      "children": []
    },
    "note": {
      "type": "input",
      "props": { "kind": "textarea", "name": "decision_note", "label": "Internal note", "placeholder": "Optional…", "required": false, "value": { "$bindState": "/decision_note" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "divider": { "type": "divider", "props": {}, "children": [] },
    "actions": {
      "type": "list",
      "props": { "direction": "horizontal", "gap": "md" },
      "children": ["approveBtn", "rejectBtn"]
    },
    "approveBtn": {
      "type": "button",
      "props": { "label": "Approve refund", "variant": "primary", "disabled": null },
      "on": { "press": { "action": "submit", "params": { "intent": "approve" } } },
      "children": []
    },
    "rejectBtn": {
      "type": "button",
      "props": { "label": "Reject", "variant": "danger", "disabled": null },
      "on": { "press": { "action": "submit", "params": { "intent": "reject" } } },
      "children": []
    }
  },
  "state": { "decision_note": "" }
}
```

### Recipe 5 — Settings panel (checkboxes, numbers, select)

A grouped settings form. `number`/`integer` come back as numbers; checkboxes as booleans.

```json theme={null}
{
  "root": "form",
  "elements": {
    "form": {
      "type": "form",
      "props": { "title": "Notification settings", "description": "Choose how this agent contacts you." },
      "children": ["channel", "frequency", "digest", "quiet", "saveBtn"]
    },
    "channel": {
      "type": "input",
      "props": { "kind": "select", "name": "channel", "label": "Channel", "placeholder": null, "required": true, "value": { "$bindState": "/channel" }, "options": [ { "value": "email", "label": "Email" }, { "value": "sms", "label": "SMS" }, { "value": "none", "label": "Off" } ], "min": null, "max": null },
      "children": []
    },
    "frequency": {
      "type": "input",
      "props": { "kind": "integer", "name": "max_per_day", "label": "Max per day", "placeholder": null, "required": false, "value": { "$bindState": "/max_per_day" }, "options": null, "min": 0, "max": 50 },
      "children": []
    },
    "digest": {
      "type": "input",
      "props": { "kind": "checkbox", "name": "weekly_digest", "label": "Send a weekly digest", "placeholder": null, "required": false, "value": { "$bindState": "/weekly_digest" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "quiet": {
      "type": "input",
      "props": { "kind": "checkbox", "name": "quiet_hours", "label": "Respect quiet hours (9pm–8am)", "placeholder": null, "required": false, "value": { "$bindState": "/quiet_hours" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "saveBtn": {
      "type": "button",
      "props": { "label": "Save settings", "variant": "primary", "disabled": null },
      "on": { "press": { "action": "submit", "params": { "intent": "save" } } },
      "children": []
    }
  },
  "state": { "channel": "email", "max_per_day": 5, "weekly_digest": true, "quiet_hours": false }
}
```

### Recipe 6 — Read-only dashboard

A static summary mixing stats, badges, an alert, and a table. No `state`, no buttons — perfect for an inline artifact or a static `render_ui` call.

```json theme={null}
{
  "root": "dash",
  "elements": {
    "dash": {
      "type": "card",
      "props": { "title": "This week", "subtitle": "Mon–Sun" },
      "children": ["metrics", "health", "divider", "table"]
    },
    "metrics": {
      "type": "list",
      "props": { "direction": "horizontal", "gap": "lg" },
      "children": ["revenue", "convos", "csat"]
    },
    "revenue": { "type": "stat", "props": { "label": "Revenue", "value": "12,450", "change": "+8%", "changeType": "up", "prefix": "$", "suffix": null }, "children": [] },
    "convos":  { "type": "stat", "props": { "label": "Conversations", "value": 342, "change": "+12%", "changeType": "up", "prefix": null, "suffix": null }, "children": [] },
    "csat":    { "type": "stat", "props": { "label": "Satisfaction", "value": "94%", "change": "-1%", "changeType": "down", "prefix": null, "suffix": null }, "children": [] },
    "health": {
      "type": "alert",
      "props": { "title": null, "message": "All systems operational.", "variant": "success" },
      "children": []
    },
    "divider": { "type": "divider", "props": {}, "children": [] },
    "table": {
      "type": "table",
      "props": { "caption": "Top issues", "columns": ["Issue", "Count", "Resolved"], "rows": [ ["Billing", 87, 81], ["Password reset", 65, 65], ["Feature requests", 44, 12] ] },
      "children": []
    }
  },
  "state": {}
}
```

### Recipe 7 — Dynamic line items

`repeat` renders a row per array item; `pushState` / `removeState` add and remove them; `$bindItem` binds each row's input back into the item. The Add button generates an id with `"$id"` and clears the draft field.

```json theme={null}
{
  "root": "card",
  "elements": {
    "card": {
      "type": "card",
      "props": { "title": "Invoice line items", "subtitle": null },
      "children": ["rows", "adder", "submitBtn"]
    },
    "rows": {
      "type": "list",
      "props": { "direction": "vertical", "gap": "sm" },
      "repeat": { "statePath": "/items", "key": "id" },
      "children": ["row"]
    },
    "row": {
      "type": "list",
      "props": { "direction": "horizontal", "gap": "sm" },
      "children": ["rowName", "rowQty", "removeBtn"]
    },
    "rowName": {
      "type": "input",
      "props": { "kind": "text", "name": "name", "label": null, "placeholder": "Description", "required": true, "value": { "$bindItem": "name" }, "options": null, "min": null, "max": null },
      "children": []
    },
    "rowQty": {
      "type": "input",
      "props": { "kind": "integer", "name": "qty", "label": null, "placeholder": "Qty", "required": true, "value": { "$bindItem": "qty" }, "options": null, "min": 1, "max": 999 },
      "children": []
    },
    "removeBtn": {
      "type": "button",
      "props": { "label": "Remove", "variant": "ghost", "disabled": null },
      "on": { "press": { "action": "removeState", "params": { "statePath": "/items", "index": { "$index": true } } } },
      "children": []
    },
    "adder": {
      "type": "button",
      "props": { "label": "Add line", "variant": "secondary", "disabled": null },
      "on": { "press": { "action": "pushState", "params": { "statePath": "/items", "value": { "id": "$id", "name": "", "qty": 1 } } } },
      "children": []
    },
    "submitBtn": {
      "type": "button",
      "props": { "label": "Save invoice", "variant": "primary", "disabled": null },
      "on": { "press": { "action": "submit" } },
      "children": []
    }
  },
  "state": { "items": [ { "id": "seed1", "name": "", "qty": 1 } ] }
}
```

### Recipe 8 — Live summary with `$template`

A display prop reads state directly, so a summary updates as the user types. (`$template` interpolates; it doesn't do arithmetic — echo values, don't compute totals.)

```json theme={null}
{
  "root": "card",
  "elements": {
    "card": { "type": "card", "props": { "title": "Book a table", "subtitle": null }, "children": ["name", "guests", "date", "summary", "submitBtn"] },
    "name":   { "type": "input", "props": { "kind": "text", "name": "name", "label": "Name", "placeholder": null, "required": true, "value": { "$bindState": "/name" }, "options": null, "min": null, "max": null }, "children": [] },
    "guests": { "type": "input", "props": { "kind": "integer", "name": "guests", "label": "Guests", "placeholder": null, "required": true, "value": { "$bindState": "/guests" }, "options": null, "min": 1, "max": 20 }, "children": [] },
    "date":   { "type": "input", "props": { "kind": "text", "name": "date", "label": "Date", "placeholder": "2026-06-01", "required": true, "value": { "$bindState": "/date" }, "options": null, "min": null, "max": null }, "children": [] },
    "summary": { "type": "alert", "props": { "title": "Your booking", "message": { "$template": "Table for ${/guests} under ${/name} on ${/date}." }, "variant": "info" }, "children": [] },
    "submitBtn": { "type": "button", "props": { "label": "Request booking", "variant": "primary", "disabled": null }, "on": { "press": { "action": "submit" } }, "children": [] }
  },
  "state": { "name": "", "guests": 2, "date": "" }
}
```

### Recipe 9 — In-render wizard with `setState`

`setState` flips a `/step` flag and `visible` shows the matching panel — a multi-step flow inside a single render, no agent round-trip per step.

```json theme={null}
{
  "root": "wizard",
  "elements": {
    "wizard": { "type": "card", "props": { "title": "Setup", "subtitle": null }, "children": ["step1", "step2"] },
    "step1": {
      "type": "card",
      "visible": [{ "$state": "/step", "eq": 2, "not": true }],
      "props": { "title": "Step 1 — Your name", "subtitle": null },
      "children": ["name", "next"]
    },
    "name": { "type": "input", "props": { "kind": "text", "name": "name", "label": "Name", "placeholder": null, "required": true, "value": { "$bindState": "/name" }, "options": null, "min": null, "max": null }, "children": [] },
    "next": { "type": "button", "props": { "label": "Next", "variant": "primary", "disabled": null }, "on": { "press": { "action": "setState", "params": { "statePath": "/step", "value": 2 } } }, "children": [] },
    "step2": {
      "type": "card",
      "visible": { "$state": "/step", "eq": 2 },
      "props": { "title": "Step 2 — Confirm", "subtitle": null },
      "children": ["who", "back", "finish"] },
    "who": { "type": "alert", "props": { "title": null, "message": { "$template": "Creating an account for ${/name}." }, "variant": "info" }, "children": [] },
    "back": { "type": "button", "props": { "label": "Back", "variant": "ghost", "disabled": null }, "on": { "press": { "action": "setState", "params": { "statePath": "/step", "value": 1 } } }, "children": [] },
    "finish": { "type": "button", "props": { "label": "Create account", "variant": "primary", "disabled": null }, "on": { "press": { "action": "submit" } }, "children": [] }
  },
  "state": { "step": 1, "name": "" }
}
```

## Guiding the agent

The agent decides when to render UI. Steer it from your **Purpose** or **Rules**:

> When a support ticket needs a resolution choice, use `render_ui` (interactive) to show the order details as a `key_value` and the options as a `select`, rather than asking in plain text.

For deterministic UI from your own systems, build a [custom tool](/sidebar-menu/tools) that returns a `_render` payload — the agent shows exactly what your code produces.

## Limits and validation

* **Allowed components only:** the eleven listed above. Anything else is rejected.
* **Size:** up to **200 elements** and a nesting depth of **6**.
* **Every prop key must be present** (use `null` for unused ones).
* **Inputs:** `kind` not `type`; `select` needs object `options`; every input needs a `name`; bind `value` to collect it.
* Invalid specs return a tool error with a `details.hint` so the agent can self-correct on the next turn.

## Limitations

A few things in the broader json-render spec aren't rendered — use the supported pattern instead:

| Not supported                                                          | Use instead                                                                                                                                                                   |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Input kinds `date`, `radio`, `multiselect`                             | `text` for dates, `select` for single choice, several `checkbox`es for multi-select.                                                                                          |
| Expressions inside `table.rows`, `table.columns`, or `key_value.items` | These are literal-only. Put dynamic text in a top-level scalar prop (an `alert.message`, a repeated row's input, …), or precompute the table from data your tool already has. |
| Arithmetic in `$template` (e.g. a computed total)                      | `$template` interpolates values, it doesn't compute. Calculate on the agent and pass the result in `state`.                                                                   |

## See also

* [`render_ui`](/builtin-tools/render_ui) — the built-in tool reference.
* [`collect_form`](/builtin-tools/collect_form) — a simpler "collect these fields" tool when you don't need a custom layout.
* [`confirm_action`](/builtin-tools/confirm_action) / [`commit_action`](/builtin-tools/commit_action) — gate destructive submissions.
* [Custom tools](/sidebar-menu/tools) — return `_render` payloads from your own API or code.
