All visual components follow the json-render spec, but Agentheya ships its own renderer with a focused, well-supported subset. Stick to what’s documented here — see Limitations for the features that exist in the underlying spec but are not rendered by Agentheya yet.
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.render_ui tool
A built-in tool the agent calls to draw UI on demand. Can be static or interactive. Enable it on the Tools tab.
Custom tool response
Your own API/code tool returns a
_render payload and the agent displays it. Can be interactive.Inline artifact
The agent embeds a read-only component inside its text reply when a visual is clearer than prose. Always static.
The spec model
A spec is a flat map of elements plus a pointer to the root and an optional state object:root— the id (a key inelements) of the top-level element to render.elements— a map ofid → element. Elements are not nested; nesting is expressed by listing child ids inchildren.state— the data model. Seed it with the starting values for any input fields (see Making it dynamic).
Component reference
Eleven components are available. Each table lists all props (remember: all are required-present; usenull for unused ones). Allowed enum values are shown in code.
Containers
These hold other elements viachildren.
card
A titled panel.list
A flex container that lays its children in a row or column.form
Groups inputs and a submit button. Visually a labelled section; submission still happens via abutton wired to the submit action (see Interactivity).
divider
A horizontal rule. No props.Display
Read-only components for showing data. All are leaf elements (children: []).
stat
A single KPI: label, value, and an optional trend.badge
A small inline label / chip.alert
A banner notice. Usevariant for severity.
key_value
A definition list of label/value pairs.table
A simple data table.Interactive
These collect input or trigger submission. They only do anything when the render is interactive (therender_ui tool sets interactive: true, or a custom tool sets _interactive: true).
button
A clickable button. Bind its behaviour withon.press.
input
A form field. Thekind 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.
kind values: text, email, number, integer, tel, url, password, textarea, select, checkbox.
Field rules worth memorising:
- Use
kind(nottype) to choose the input variant. selectoptionsmust 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/integerreturn a number (ornullwhen empty);checkboxreturns a boolean; everything else returns a string.- There is no
date,radio, ormultiselectkind. Usetextfor dates andselectfor single choice. (Multi-select can be modelled as severalcheckboxfields.)
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 (/contact/email, not contact.email).
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.Conditional visibility
Any element can carry a top-levelvisible 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.
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-levelrepeat to render its children once per item in a state array. The element itself renders once as the container; its children expand per item.
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).
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
statein 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).
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:
The current form
state is sent with every click, so the code / tool sees up-to-date input values.
runCode — sandboxed snippet
params.code is the source. params.lang is "python" (default) or "javascript". The runtime, sandbox isolation, and limits match those of the execute_code tool.
callTool — invoke a registered tool
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:
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:
What the agent receives
After the user acts, the agent’s next turn gets:values is the whole state model — every $bindState path becomes a key. (Cancellation arrives as { "status": "cancelled" } with no values.)
1
Agent renders an interactive form
It calls
render_ui with interactive: true (or your tool returns _render + _interactive: true) and then ends its turn.2
User fills it in and clicks a button
Inputs write to
state as they type; the submit/cancel action fires on click.3
Agent reads the result next turn
The submitted
values / action / params arrive as the tool result, and the agent continues.Form recipes
Each recipe below is a complete, validspec (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 invalues.
Recipe 2 — Conditional fields (reveal on selection)
The contact-methodselect drives which follow-up field is visible, using visible + $bindState. No scripting required — picking an option re-renders the dependent fields.
Recipe 3 — Reveal a whole section
Avisible 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.)
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.
Recipe 5 — Settings panel (checkboxes, numbers, select)
A grouped settings form.number/integer come back as numbers; checkboxes as booleans.
Recipe 6 — Read-only dashboard
A static summary mixing stats, badges, an alert, and a table. Nostate, no buttons — perfect for an inline artifact or a static render_ui call.
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.
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.)
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.
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, useFor deterministic UI from your own systems, build a custom tool that returns arender_ui(interactive) to show the order details as akey_valueand the options as aselect, rather than asking in plain text.
_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
nullfor unused ones). - Inputs:
kindnottype;selectneeds objectoptions; every input needs aname; bindvalueto collect it. - Invalid specs return a tool error with a
details.hintso 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:See also
render_ui— the built-in tool reference.collect_form— a simpler “collect these fields” tool when you don’t need a custom layout.confirm_action/commit_action— gate destructive submissions.- Custom tools — return
_renderpayloads from your own API or code.