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.

WidgetUse it to…Answer value
ConfirmGet a yes/no before actingtrue or false
ChoicePick one or several options"id", ["id", …] or { selected, other }
PermissionAsk for access, once or for the session{ granted, scope }
FormCollect typed fields{ fieldName: value, … }
Date and timePick a date, a time or both{ mode, value }
File pickerAsk for one or more files{ files: [...] }
ProgressShow 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" },
  });
});
Give every widget a meaningful ID

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 }.

Enforce permissions in code

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.