> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# form

> An inline form the end user fills in and submits in one go, with a YAML body, show/hide logic and a structured answer

`form` shows an inline form in the chat. The end user fills in every field and submits once, and your agent receives one structured answer. Use it when you need several details together, such as a booking, an address or an inspection checklist. For one to four quick multiple-choice questions, the model uses choice cards instead.

The platform teaches the model this block only on chat apps that can show it. Those apps declare the `forms-v1` capability; see [Where it renders](#where-it-renders). Elsewhere the model uses it only when your persona or skill context includes the syntax, or when a tool returns a ready-made block. A form a channel can't show is sent as its questions in plain text, so the end user can still answer.

## Syntax

The body between `::: form` and `:::` is YAML. JSON also works.

```text theme={null}
::: form
id: table-booking
title: Book a table
fields:
  - type: date
    key: day
    label: Day
    min: today
    required: true
  - type: time
    key: at
    label: Time
    required: true
  - type: number
    key: guests
    label: Guests
    min: 1
    max: 12
    required: true
  - type: choice
    key: area
    label: Seating
    options: [Inside, Terrace]
  - type: textarea
    key: notes
    label: Anything we should know?
    visible_if: { field: area, equals: Terrace }
submit: Request booking
:::
```

YAML rules:

* Quote a value that contains `:` or `#`, or that starts with a symbol.
* Use `|` for text that spans several lines.
* Never put `:::` inside the body, and never wrap the form in a code fence.

Values are read as written, so phone numbers and codes keep their leading `+` and `0`.

## Form fields

| Field | Required | Notes |
| - | - | - |
| `id` | No | A short name such as `table-booking`. The answer carries it, so set it whenever a tool reads the answer. |
| `title`, `description` | No | Shown above the fields. The description takes Markdown. |
| `fields` | Yes | The fields and text, in order. At most 50. |
| `submit` | No | The button label. Defaults to "Submit". |
| `fallback_text` | No | What channels that can't show the form send instead. Without it, the questions are listed as text. |
| `version` | No | Defaults to `1`. |

## Field types

| Type | The answer | Options |
| - | - | - |
| `heading`, `subheading`, `paragraph`, `caption` | none | `text`. `paragraph` and `caption` take Markdown. |
| `image` | none | `src` (HTTPS), `alt` |
| `link` | none | `text`, `url` (HTTPS, `mailto:` or `tel:`) |
| `divider` | none | none |
| `text` | text | `format` (`email`, `phone`, `url`, `password`, `passcode`), `min_length`, `max_length`, `pattern` |
| `textarea` | text | `min_length`, `max_length` |
| `number` | a number | `min`, `max`, `step` |
| `date` | `YYYY-MM-DD` | `min`, `max` (a date, `today`, or `today+N` / `today-N`), `unavailable` (a list of dates) |
| `date_range` | `{start, end}`, both days included | `min`, `max`, `min_days`, `max_days` |
| `time` | `HH:mm` | `min`, `max`, `step_minutes` |
| `datetime` | `YYYY-MM-DDTHH:mm` | `min`, `max` |
| `choice` | the chosen value, or a list when `multiple: true` | `options`, `multiple`, `appearance` (`radio`, `checkbox`, `dropdown`, `chips`), `min_selected`, `max_selected` |
| `boolean` | `true` or `false` | `appearance` (`checkbox`, `switch`, `yes_no`), `link` |
| `rating` | a whole number | `min` (0 or 1), `max` (up to 10) |
| `file` | a list of `{url, name, media_type, size}` | `accept` (for example `[image/*]`), `max_files` (up to 10), `max_size_mb` (up to 25), `source` (`any`, `camera`, `gallery`) |

`options` is a list of labels, or a list of `{ value, label, description, image }` entries when you want stable values that differ from the labels. Without an `appearance`:

* a choice with up to 7 options shows as radio buttons, or checkboxes for `multiple`;
* a choice with more options shows as a dropdown.

A `boolean` with `required: true` must be ticked, which suits consent. With `appearance: yes_no`, it must be answered either way.

Common names are accepted as shorthand:

| You write | It works as |
| - | - |
| `email`, `phone`, `url`, `password` | a `text` with that `format` |
| `radio`, `dropdown`, `chips` | a `choice` with that `appearance` |
| `checkboxes` | a `choice` with `multiple: true` |
| `yes_no`, `consent` | a `boolean` |
| `photo` | a `file` that accepts images |
| `document` | a `file` that accepts documents |

### Every input

| Option | Notes |
| - | - |
| `key` | The name of the answer. Unique in the form; letters, digits and `_`, starting with a letter. |
| `label` | The question. |
| `help`, `placeholder` | A hint under the field, and placeholder text. |
| `required` | `true` when an answer is needed. |
| `default` | A prefilled answer, in the same shape as the answer. |
| `read_only` | Shows the `default` without letting the end user change it. |
| `error` | Your own message when the answer is invalid. |
| `row` | Fields with the same `row` sit side by side when there is room. |
| `visible_if` | Shows the field only when an earlier answer matches. See [Conditions](#conditions). |

## Conditions

`visible_if` names an earlier field and one test:

```text theme={null}
visible_if: { field: counters, equals: Fail }
visible_if: { field: guests, gt: 6 }
visible_if: { field: extras, contains: parking }
visible_if: { field: notes, empty: false }
```

| Test | Meaning |
| - | - |
| `equals`, `not_equals` | The answer is (or isn't) this value. |
| `in`, `not_in` | The answer is one of a list. |
| `contains` | A `multiple` choice includes this value. |
| `gt`, `gte`, `lt`, `lte` | Comparisons for numbers, dates and times. |
| `empty` | `true` when left blank, `false` when answered. |

Combine tests with `all`, `any` and `not`:

```text theme={null}
visible_if:
  all:
    - { field: area, equals: Terrace }
    - { field: guests, gt: 6 }
```

A hidden field is neither checked nor sent, and a field that depends on a hidden one counts it as blank.

## The answer

When the end user submits, the chat sends one message with the answers, keyed by `key`:

```text theme={null}
::: form-response
{"form":"table-booking","status":"submitted","values":{"day":"2026-11-02","at":"19:30","guests":4,"area":"Terrace","notes":"A birthday"},"tz":"Europe/Bucharest"}
:::
```

* **`status`.** `"declined"` means the end user chose to type an answer instead, and `values` is empty.
* **`tz`.** The time zone of the device, for dates and times.
* **Files.** File answers point at uploaded files. Uploaded photos also reach the model as images.

The chat app shows this message as a short "Submitted" card, and the form above it stays answered after a reload. The model reads the block directly. A tool can also read it from the message, because the JSON is on a single line.

## Generating a form from a tool

A tool can return a form it builds from data, for example one checklist question per item, and tell the model to send the block as is. That keeps the form exact and leaves nothing to the model. Build it as an object and serialize it with `JSON.stringify`, which escapes quotes and line breaks in your data. JSON is valid inside the block. Keep `:::` out of the values, because it would end the block early:

```ts theme={null}
const form = {
  id: 'daily-audit',
  title: 'Daily audit',
  fields: items.map((item) => ({
    type: 'radio',
    key: item.key,
    label: item.label.replaceAll(':::', ''),
    options: ['Pass', 'Fail', 'N/A'],
    required: true,
  })),
};
return `::: form\n${JSON.stringify(form, null, 2)}\n:::`;
```

## Where it renders

| Surface | Behavior |
| - | - |
| Web widget, Lua Desktop, the dashboard chat, the mobile app, Frontline | The form, once that app declares `forms-v1`. An older version shows the questions as text. |
| WhatsApp, Messenger, Instagram, Slack, Teams, RCS, iMessage, MessageBird, SMS, Front | The questions as numbered text, ending "Reply with your answers." |
| Email (both forms) | The questions as text |
| Voice | Dropped |

## Limits

| Limit | Value |
| - | - |
| Body size | 32 KB |
| Fields | 50 |
| Options per choice | 200 |
| Files per field | 10, up to 25 MB each |

The older pipe syntax (`key | Label | type | required`) still renders, but write new forms in YAML.

## See also

* [Response formatting](/channels/formatting/overview)
* [`flow`](/channels/formatting/flow), the WhatsApp Flows block


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.