Widgets
Integration samples for every supported widget in the JavaScript and Python SDKs.
Widgets are structured prompts inside a conversation. Use them when an agent needs approval, a choice, a form, progress feedback, a date/time value, or a file from the user.
Every widget uses the same pattern:
- Send the widget from the agent handler.
- Wait for the user to answer in the app.
- Read the answer from the next message's widget response.
- Continue the local task.
Handle widget responses
JavaScript
client.onMessage(async (message, agent) => {
if (!message.widgetResponse) return;
const { ref, value, cancelled } = message.widgetResponse;
if (cancelled) {
await agent.sendMessage({
conversationId: message.conversationId,
to: message.from,
text: `Cancelled: ${ref}`,
});
return;
}
console.log(ref, value);
});
Python
async def handle(message, agent):
if not message.widget_response:
return
response = message.widget_response
if response.cancelled:
await agent.send_message(
conversation_id=message.conversation_id,
to=message.from_id,
text=f"Cancelled: {response.ref}",
)
return
print(response.ref, response.value)
Confirmation
Use a confirmation widget before destructive or expensive work.
JavaScript
await agent.sendConfirmWidget({
conversationId: message.conversationId,
to: message.from,
id: "confirm-delete-cache",
title: "Delete local cache?",
body: "The agent will remove generated cache files from this machine.",
danger: true,
labels: { yes: "Delete", no: "Keep files" },
});
Python
await agent.send_confirm_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="confirm-delete-cache",
title="Delete local cache?",
body="The agent will remove generated cache files from this machine.",
danger=True,
labels={"yes": "Delete", "no": "Keep files"},
)
Response value
true
Handle the response
if (message.widgetResponse?.ref === "confirm-delete-cache") {
if (message.widgetResponse.value === true) await deleteCache();
}
Choice
Use choice when the user must select one or more options.
JavaScript
await agent.sendChoiceWidget({
conversationId: message.conversationId,
to: message.from,
id: "choose-export-format",
title: "Choose export format",
body: "The agent will generate the selected file type.",
options: [
{ id: "pdf", label: "PDF" },
{ id: "docx", label: "Word document" },
{ id: "md", label: "Markdown" },
],
});
Python
await agent.send_choice_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="choose-export-format",
title="Choose export format",
body="The agent will generate the selected file type.",
options=[
{"id": "pdf", "label": "PDF"},
{"id": "docx", "label": "Word document"},
{"id": "md", "label": "Markdown"},
],
)
Response value
"pdf"
Multiple choices
For multiple choices, set multi: true in JavaScript or pass multi=True in Python:
await agent.sendChoiceWidget({
conversationId: message.conversationId,
to: message.from,
id: "choose-checks",
title: "Choose checks to run",
multi: true,
options: [
{ id: "lint", label: "Lint" },
{ id: "test", label: "Tests" },
{ id: "build", label: "Build" },
],
});
await agent.send_choice_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="choose-checks",
title="Choose checks to run",
multi=True,
options=[
{"id": "lint", "label": "Lint"},
{"id": "test", "label": "Tests"},
{"id": "build", "label": "Build"},
],
)
Multiple-choice response value:
["lint", "test"]
Permission
Use permission when the user must grant a scope for an action.
JavaScript
await agent.sendPermissionWidget({
conversationId: message.conversationId,
to: message.from,
id: "allow-folder-read",
title: "Allow folder read?",
body: "The agent wants to inspect files in ./docs before answering.",
scopes: ["once", "session"],
});
Python
await agent.send_permission_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="allow-folder-read",
title="Allow folder read?",
body="The agent wants to inspect files in ./docs before answering.",
scopes=["once", "session"],
)
Response value
{ "granted": true, "scope": "once" }
Handle the response
if (message.widgetResponse?.ref === "allow-folder-read") {
const permission = message.widgetResponse.value as { granted?: boolean; scope?: string };
if (!permission.granted) return;
await readDocsFolder({ scope: permission.scope });
}
Form
Use a form when the agent needs typed fields instead of a free-text answer.
JavaScript
await agent.sendFormWidget({
conversationId: message.conversationId,
to: message.from,
id: "release-form",
title: "Release details",
body: "Fill this before the agent starts the release task.",
submitLabel: "Start release",
fields: [
{ name: "version", label: "Version", type: "text", required: true },
{ name: "priority", label: "Priority", type: "number", min: 1, max: 5 },
{ name: "notes", label: "Notes", type: "textarea" },
{ name: "announce", label: "Announce in chat", type: "checkbox" },
],
});
Python
await agent.send_form_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="release-form",
title="Release details",
body="Fill this before the agent starts the release task.",
submitLabel="Start release",
fields=[
{"name": "version", "label": "Version", "type": "text", "required": True},
{"name": "priority", "label": "Priority", "type": "number", "min": 1, "max": 5},
{"name": "notes", "label": "Notes", "type": "textarea"},
{"name": "announce", "label": "Announce in chat", "type": "checkbox"},
],
)
Response value
{
"version": "1.2.0",
"priority": 3,
"notes": "Ship after QA signs off.",
"announce": true
}
Progress
Use progress for work that takes more than a few seconds.
JavaScript
const progressId = await agent.sendProgressWidget({
conversationId: message.conversationId,
to: message.from,
id: "build-progress",
title: "Building project",
body: "The agent is running install, tests, and build.",
value: 10,
max: 100,
cancellable: true,
});
await agent.sendWidgetUpdate({
conversationId: message.conversationId,
to: message.from,
ref: progressId,
spec: { value: 75, body: "Running tests" },
});
Python
progress_id = await agent.send_progress_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="build-progress",
title="Building project",
body="The agent is running install, tests, and build.",
value=10,
max=100,
cancellable=True,
)
await agent.send_widget_update(
conversation_id=message.conversation_id,
to=message.from_id,
ref=progress_id,
spec={"value": 75, "body": "Running tests"},
)
Cancellation response
{ "cancelled": true }
Date and Time
Use date/time when the user must choose a calendar or schedule value.
JavaScript
await agent.sendDateTimeWidget({
conversationId: message.conversationId,
to: message.from,
id: "schedule-review",
title: "Schedule review",
body: "Pick a review date.",
mode: "date",
min: "2026-05-23",
});
Python
await agent.send_datetime_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="schedule-review",
title="Schedule review",
body="Pick a review date.",
mode="date",
min="2026-05-23",
)
Modes
Use mode: "time" for a time picker and mode: "datetime" for a date-time picker.
Response value:
{ "mode": "date", "value": "2026-05-24" }
File Picker
Use file picker when the user must provide local files to the agent.
JavaScript
await agent.sendFilePickerWidget({
conversationId: message.conversationId,
to: message.from,
id: "upload-brief",
title: "Upload project brief",
body: "Attach the PDF or Markdown brief the agent should read.",
multiple: true,
accept: ["application/pdf", ".md"],
maxFiles: 3,
});
Python
await agent.send_file_picker_widget(
conversation_id=message.conversation_id,
to=message.from_id,
widget_id="upload-brief",
title="Upload project brief",
body="Attach the PDF or Markdown brief the agent should read.",
multiple=True,
accept=["application/pdf", ".md"],
max_files=3,
)
Response value
{
"files": [
{
"id": "attachment-id",
"name": "brief.pdf",
"mime": "application/pdf",
"size": 48291,
"blob_id": "blob-id"
}
]
}
Handle uploaded files
The SDK also exposes uploaded attachments on the message, so your handler can process the files after the user submits the widget.
JavaScript:
if (message.widgetResponse?.ref === "upload-brief") {
for (const file of message.attachments) {
console.log(file.name, file.mime, file.size);
}
}
Python:
if message.widget_response and message.widget_response.ref == "upload-brief":
for file in message.attachments or []:
print(file.name, file.mime, file.size)
Design rules for agents
- Use confirmation widgets for destructive or irreversible actions.
- Use permission widgets before reading local folders, writing files, or calling external services.
- Keep form fields short and explicit.
- Report progress for work that takes more than a few seconds.
- Treat file uploads as sensitive input and validate type and size locally.
- Do not ask for secrets in a widget unless the user explicitly expects that flow.
- Give every widget a stable id so responses are easy to correlate.