Generated Form
A short form the agent builds to collect structured input, with validation and inline errors.
≈ 1,330 tokens per use (tool 781 · guidance 356 · typical payload 193)How token costs are estimated
Example: Generated Form
Add the person I met at the trade show to the CRM.
When to use
- The agent needs several pieces of structured data to create or update a record: a new contact, a shipping address, an event RSVP.
- The values are the user's to provide, and the agent can't look them up.
When not to use
- You need one or two decisions. Use the Question Stepper or ask in a sentence.
- The agent can find the values in data, files or earlier messages. Look them up, and prefill or skip those fields.
- The user is choosing among records the agent fetched. Use the Entity Picker.
How the agent should use it
- Never ask for passwords, payment card numbers, bank details or government IDs. Send the user to the system's own secure page instead.
- Keep it to 10 fields or fewer, in the order the user thinks about them, with required fields first.
- Prefill
defaultfrom real records (the saved address, the account owner) and mark only truly necessary fieldsrequired. selectoptions are data-bound. They come from the system the agent will write to, never invented.- Use
patternonly for real format rules, always with apatternMessage. - Reach for the specific type:
moneyfor an amount,phonefor a number,tagsfor a free list,mentionfor people you fetched. - Do nothing with the data until
submitarrives. Oncancel, stop.
Beyond text: money, phone, tags and mentions
Four field types cover what text fields get wrong:
| Type | What the user sees | Value in the result |
|---|---|---|
money | A number box with the currency code (currency, ISO 4217; default USD). Accepts 1,234.50; rejects more decimals than the currency has (2 for USD, 0 for JPY) | A number, in major units |
phone | A telephone box (7–15 digits, with spaces, dots, dashes and brackets allowed) | The trimmed text as typed. It's a plausibility check, not a lookup |
tags | Chips: type, then Enter or a comma to add; Backspace on an empty box removes the last. Repeats are dropped; min/max set the count | An array of strings, or null when empty |
mention | A text area where typing @ opens the field's options as a listbox (a full ARIA combobox: arrows, Enter or Tab to choose, Escape to dismiss) | The text, with chosen names as @Name. The result also carries mentions: { <fieldId>: [ids] }, read from the final text, so edits are respected |
money is an amount the user decides (a budget, a price cap), never card or account numbers. mention options are data-bound: the people or records you fetched, with value their id and label their name.
{
action: "submit",
values: { budget: 24000, topics: ["launch", "retention"], notes: "Review with @Priya Nair" },
mentions: { notes: ["usr_priya"] },
}Validation
| When | What happens |
|---|---|
| Leaving a text field | The field is checked and its error shown inline |
| Choosing an option or ticking a checkbox | Checked straight away |
| Submit | Every field is checked. Focus moves to the first invalid field, and the footer says how many need attention |
Errors are linked to their control with aria-describedby, and invalid controls carry aria-invalid. Checks cover required, email format, number and money min/max, money precision, phone plausibility, tag count, text length, date bounds, select options and pattern. The rules are exported as validateFormField, so a server can re-run them.
Installation
npx shadcn@latest add https://your-site.com/r/generated-form.json
Usage
import { tool } from "ai"
import { generatedFormTool } from "@/components/genui/generated-form.schema"
const tools = {
// No execute(): the call is completed by the user's submission in the UI.
[generatedFormTool.name]: tool({
description: generatedFormTool.description,
inputSchema: generatedFormTool.inputSchema,
}),
}import { GeneratedForm } from "@/components/genui/generated-form"
function ToolPart({ part, addToolResult }) {
if (part.type !== "tool-fill_form") return null
return (
<GeneratedForm
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({
tool: "fill_form",
toolCallId: part.toolCallId,
output: result,
})
}
/>
)
}The agent receives one of:
{ action: "submit", values: { first_name: "Dana", guests: 1, save: true, notes: null } }
{ action: "cancel" }Values are keyed by field id. number and money fields are numbers, checkboxes are booleans, tags are arrays, and empty optional fields are null. When a mention field names someone, the result also has mentions.
With LangGraph
Push it from a graph node with ui.push({ name: "fill_form", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored result back as result. The form renders the submitted values read-only (or the cancelled state) and never calls onResult again.
<GeneratedForm
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
title | string | What the form is for: "New contact" |
description | string? | One quiet line under the title, such as the venue and time. Skip it when it restates the title |
fields | FormField[] | 1–10 |
submitLabel | string? | Verb for the submit button: "Create contact". Default "Submit" |
FormField
| Field | Type | Notes |
|---|---|---|
id | string | Key in the result's values |
label | string | |
type | text | email | number | textarea | select | checkbox | date | money | phone | tags | mention | Default text |
required | boolean | Default false |
placeholder | string? | An example value |
description | string? | Help for the field. Read to screen readers with the control, not shown; put constraints the user must see in the label or placeholder |
default | string | number | boolean | Data-bound when it comes from a record. Dates as YYYY-MM-DD |
currency | string? | ISO 4217 code for money. Default USD |
options | { value, label }[] | Data-bound. Required for select and mention |
min / max | number | string | Number or money range, text length, tag count, or date bounds (YYYY-MM-DD) |
pattern | string? | Regular expression the whole value must match |
patternMessage | string? | Shown when the pattern fails |
Host props: status (streaming, ready, submitting, submitted, cancelled), onResult, result and className.