form
Typed fields and one submit, returned as a form submission.
Structured input, when a sentence will not do. form collects between one and ten typed fields and sends them back as a form_submission, a separate request shape from a message, so the values arrive typed rather than parsed back out of prose.
Six field kinds cover what a copilot actually needs: text, number, select, toggle, date and time.
Re-rendered in the browser from the SDK’s own scales. Not a screenshot.
Rendered natively by the Flutter SDK and the Web SDK, both since 0.1.0.
When not to use it
Do not use a form for one question with a handful of answers. That is quick_replies, and it is one tap instead of two.
Do not use a time field to book an appointment: the user cannot know which times are free, so offer datetime_slots instead. And do not use a form to approve something: approval is confirm_card, built server-side.
Props
Every block also carries type and a required fallback_text of 1 to 300 characters.
| Prop | Type | Required | Constraints |
|---|---|---|---|
form_id | string | Yes | 1 to 64 characters. Comes back with the submission. |
title | string | No | Up to 120 characters. |
fields | array | Yes | 1 to 10 entries. |
fields[].kind | text | number | select | toggle | date | time | Yes | None |
fields[].name | string | Yes | 1 to 64 characters. The key in values. |
fields[].label | string | Yes | 1 to 80 characters. |
fields[].required | boolean | No | For a toggle this means "must be on". |
fields[].placeholder | string | No | Up to 80 characters. Never used as the label. |
fields[].value | string | number | boolean | No | Prefill. Never overwrites an answer the user has already typed. |
fields[].options | array | No | Up to 20 entries. A select needs them, but the schema does not enforce it; one that arrives without them renders inert. |
fields[].options[].label | string | Yes | Up to 60 characters. |
fields[].options[].value | string | Yes | Up to 60 characters. |
submit_label | string | Yes | 1 to 40 characters. |
On the wire
{
"id": "blk_11",
"type": "form",
"fallback_text": "A return request form: order number, reason, number of pairs.",
"props": {
"form_id": "return_request",
"title": "Start a return",
"fields": [
{
"kind": "text",
"name": "order_id",
"label": "Order number",
"required": true,
"placeholder": "1042"
},
{
"kind": "select",
"name": "reason",
"label": "Reason",
"required": true,
"options": [
{
"label": "Too small",
"value": "too_small"
},
{
"label": "Arrived damaged",
"value": "damaged"
}
]
},
{
"kind": "number",
"name": "pairs",
"label": "How many pairs",
"required": true
},
{
"kind": "toggle",
"name": "keep_box",
"label": "I still have the original box"
}
],
"submit_label": "Request pickup"
}
}Interaction
Submit validates, then posts:
{
"form_submission": {
"form_id": "return_request",
"values": { "order_id": "1042", "reason": "damaged", "pairs": 2, "keep_box": true }
}
}Values are string, number or boolean. A number field is parsed and sent as a number; date sends YYYY-MM-DD and time sends HH:mm, both as strings.
Half-filled values are stored on the block itself, so scrolling a form out of the thread and back does not empty it.
States
| State | What the SDK draws |
|---|---|
| Invalid | Errors appear under the field they belong to, never as a summary at the top of a form the user has already scrolled past. The field's border goes to full ink at 1.5pt. A count ("3 fields above need fixing.") sits directly above the Send button, because in a long form the per-field messages are above the fold the button is in and pressing Send otherwise looked like nothing happened. |
| Select with no options | The control renders and announces itself as not chosen, but is inert: it does not open a sheet onto an empty list. |
| Empty | A fields array with no entries, or no field carrying a name, renders fallback_text inside the plate. |
| Sending | The button reads "Sending…" and every field is disabled. |
| Sent | The form is replaced by a read-only list of what was submitted, plus a "Sent" mark. The swap is sized and crossfaded rather than jumping. |
| Turn still open | If the assistant is mid-answer the submission is refused and the form says so, rather than reporting "Sent" for a request that never left. |
| Loading | A plate with a title bar, two field boxes at 44pt and a button shape. |
Accessibility
Text and number fields merge their visible label into the field's own accessible node, so they are still named once the placeholder is gone. Select, date, time and toggle carry a complete label of their own and the visible one is hidden from the reader to avoid saying it twice. Errors are live regions.
fallback_text should describe what the form asks for, so a user who cannot see it knows whether it is worth filling in.