Build
Interactive widgets
Ask for a confirmation, a choice, a form, a date, a file or show progress — as native cards inside the conversation.
Widgets turn a question into a card the user can answer in one tap. They're sent inside the encrypted conversation like any message, rendered natively on web and mobile, and the answer comes back to your agent as a new message.
| Widget | Use it to… | Answer value |
|---|---|---|
| Confirm | Get a yes/no before acting | true or false |
| Choice | Pick one or several options | "id", ["id", …] or { selected, other } |
| Permission | Ask for access, once or for the session | { granted, scope } |
| Form | Collect typed fields | { fieldName: value, … } |
| Date and time | Pick a date, a time or both | { mode, value } |
| File picker | Ask for one or more files | { files: [...] } |
| Progress | Show the progress of a task | { cancelled: true } if cancelled |
How widgets work
Send the widget
Call a send…Widget method with a stable id. It returns the widget ID.
The user answers
The card appears in the conversation. Nothing happens on your side until the user responds.
Handle the answer
Your handler receives a new message whose widgetResponse has the widget ID in ref, the answer
in value, and cancelled if the user dismissed it.
client.onMessage(async (message, agent) => {
const answer = message.widgetResponse;
if (answer) {
if (answer.cancelled) return;
if (answer.ref === "confirm-cleanup" && answer.value === true) {
await cleanUp();
await agent.reply(message, { text: "Cache cleared." });
}
return;
}
// A regular message: ask before acting.
await agent.sendConfirmWidget({
conversationId: message.conversationId,
to: message.from,
id: "confirm-cleanup",
title: "Clear the local cache?",
body: "This removes 2.3 GB of generated files.",
danger: true,
labels: { yes: "Clear", no: "Keep" },
});
});The ID is how you match an answer to its question. Use a stable, descriptive value — or generate a unique one per request and remember what it was for.
All widgets accept title (required), body, an id (widget_id in Python) and an optional
expiry, expiresAt (expires_at), after which the card can no longer be answered.
Confirm
A yes/no question. Set danger for destructive actions and customize the button labels.
await agent.sendConfirmWidget({
conversationId: message.conversationId,
to: message.from,
id: "confirm-deploy",
title: "Deploy to production?",
body: "Version 2.4.0 — 14 commits since the last release.",
labels: { yes: "Deploy", no: "Not now" },
});Answer: true or false.
Choice
A list of options. Set multi to allow several answers.
await agent.sendChoiceWidget({
conversationId: message.conversationId,
to: message.from,
id: "export-format",
title: "Which format?",
options: [
{ id: "pdf", label: "PDF" },
{ id: "docx", label: "Word" },
{ id: "md", label: "Markdown" },
],
});Answer: the option ID ("pdf"), an array of IDs with multi (["pdf", "md"]), or
{ "selected": [...], "other": "…" } when the user types their own answer. Mark an option with
danger: true to highlight it.
Permission
Asks the user to allow an action, with the scopes you offer: once or session.
await agent.sendPermissionWidget({
conversationId: message.conversationId,
to: message.from,
id: "read-downloads",
title: "Read your Downloads folder?",
body: "To find last month's invoices.",
scopes: ["once", "session"],
});Answer: { "granted": true, "scope": "once" } or { "granted": false }.
This widget only records the user's decision. To bind an approval to an exact action, resource and duration — and refuse anything else — use the permission policy.
Form
Typed fields: text, textarea, number, email or checkbox. Each field can be required
and have a placeholder; numbers accept min and max.
await agent.sendFormWidget({
conversationId: message.conversationId,
to: message.from,
id: "trip",
title: "Plan the trip",
submitLabel: "Search",
fields: [
{ name: "destination", label: "Destination", type: "text", required: true },
{ name: "travelers", label: "Travelers", type: "number", min: 1, max: 9 },
{ name: "notes", label: "Anything else?", type: "textarea" },
{ name: "flexible", label: "Flexible dates", type: "checkbox" },
],
});Answer: an object keyed by field name —
{ "destination": "Lisbon", "travelers": 2, "notes": "", "flexible": true }.
Date and time
mode is date, time or datetime. Restrict the range with min and max (ISO 8601).
await agent.sendDateTimeWidget({
conversationId: message.conversationId,
to: message.from,
id: "meeting",
title: "When should I book the meeting?",
mode: "datetime",
min: "2026-10-01T09:00:00",
});Answer: { "mode": "datetime", "value": "2026-10-03T14:30" }.
File picker
Asks the user for files. Limit them with accept (MIME types or extensions), multiple and
maxFiles.
await agent.sendFilePickerWidget({
conversationId: message.conversationId,
to: message.from,
id: "brief",
title: "Send me the project brief",
accept: ["application/pdf", ".md"],
multiple: true,
maxFiles: 3,
});Answer: { "files": [{ "id", "name", "mime", "size", ... }] }. The files themselves arrive
in message.attachments; read them with downloadAttachment — see
Files and media.
Progress
A progress bar you update while a task runs. With cancellable, the user can ask you to stop.
const progressId = await agent.sendProgressWidget({
conversationId: message.conversationId,
to: message.from,
id: "import",
title: "Importing photos",
value: 0,
max: 100,
cancellable: true,
});
// Later, as work advances:
await agent.sendWidgetUpdate({
conversationId: message.conversationId,
to: message.from,
ref: progressId,
spec: { value: 60, body: "742 of 1,230 photos" },
});Answer: only if the user cancels — { "cancelled": true }. Stop the task and confirm.
sendWidgetUpdate works for any widget: change its spec (title, body, value…) or its expiry
after it was sent.
Good practices
- Ask before acting. Use Confirm or Permission before anything destructive, expensive or external.
- One question per widget. Short titles, one line of context in
body. - Never ask for secrets such as passwords or card numbers in a form.
- Validate answers as you would any user input — types, ranges and file sizes.
- Close the loop. After an answer, reply with what you did.
Using several agents or people in a group? Choice and Confirm widgets support collective votes.