# Modals

> Open forms from your chat app with views.open, collect input blocks, validate submissions and stack up to three views.

A modal is a dialog your app opens for one person — usually a form. It is a **view**: a title, an optional submit and close button, and up to 100 [blocks](https://workstation.repzo.com/docs/developers/block-kit/blocks), including [input blocks](https://workstation.repzo.com/docs/developers/block-kit/blocks/input-block).

## Open a modal

Modals open in answer to something the person did, so you need the `trigger_id` from an [interaction payload](https://workstation.repzo.com/docs/developers/chat-apps/interactivity). Use it within 60 seconds, once.

```http
POST /api/bot/views.open
Authorization: Bearer foxa-…
```

```json
{
  "trigger_id": "c0b6f3d1-…",
  "view": {
    "type": "modal",
    "callback_id": "refund_form",
    "title": { "type": "plain_text", "text": "Refund request" },
    "submit": { "type": "plain_text", "text": "Submit" },
    "close": { "type": "plain_text", "text": "Cancel" },
    "private_metadata": "order-4821",
    "blocks": [
      {
        "type": "input",
        "block_id": "reason",
        "label": { "type": "plain_text", "text": "Reason" },
        "element": { "type": "plain_text_input", "action_id": "value", "multiline": true, "min_length": 3 }
      },
      {
        "type": "input",
        "block_id": "amount",
        "label": { "type": "plain_text", "text": "Amount (JOD)" },
        "element": { "type": "number_input", "action_id": "value", "is_decimal_allowed": true, "min_value": "1" }
      }
    ]
  }
}
```

The answer is `{ "ok": true, "view": { "id": "…", "hash": "…", … } }`. Keep `view.id` (or set your own `external_id`) to update it later.

### View fields

| Field | Description |
|---|---|
| `type` | `modal` |
| `title` | `plain_text`, up to 24 characters. Required. |
| `submit` | Submit button label (`plain_text`, 24). Required when the view has input blocks. |
| `close` | Close button label (`plain_text`, 24). Defaults to "Cancel". |
| `blocks` | Up to 100 blocks. |
| `private_metadata` | Up to 3000 characters, returned to you in every payload from this view. |
| `callback_id` | Your name for the view, returned in payloads. |
| `external_id` | Your own unique id for the view, usable with `views.update`. |
| `clear_on_close` | Closing this view closes the whole stack. |
| `notify_on_close` | Send `view_closed` when the person closes it. |
| `submit_disabled` | Show the submit button disabled. |

## Inputs

[Input blocks](https://workstation.repzo.com/docs/developers/block-kit/blocks/input-block) hold one element each: [plain text](https://workstation.repzo.com/docs/developers/block-kit/elements/plain-text-input-element), [email](https://workstation.repzo.com/docs/developers/block-kit/elements/email-input-element), [URL](https://workstation.repzo.com/docs/developers/block-kit/elements/url-input-element), [number](https://workstation.repzo.com/docs/developers/block-kit/elements/number-input-element), [rich text](https://workstation.repzo.com/docs/developers/block-kit/elements/rich-text-input-element), [file](https://workstation.repzo.com/docs/developers/block-kit/elements/file-input-element), or any menu, picker, checkbox group or radio group. Mark a block `optional: true` to allow it empty.

Inputs don't send anything while the person types. Set `dispatch_action: true` on an input block to also get a `block_actions` payload when that element changes (text inputs dispatch on Enter, or as they type with a [dispatch action configuration](https://workstation.repzo.com/docs/developers/block-kit/objects/dispatch-action-configuration-object)).

## Handle the submission

When the person presses submit, Workstation first checks the form itself — required fields, lengths, number ranges, email and URL formats — and shows errors under the fields without bothering your app. Then it sends `view_submission`:

```json
{
  "type": "view_submission",
  "user": { "id": "42bc76d7-…", "name": "Dana Reader" },
  "api_app_id": "75385d0c-…",
  "trigger_id": "…",
  "view": {
    "id": "8d3bc164-…",
    "callback_id": "refund_form",
    "private_metadata": "order-4821",
    "state": {
      "values": {
        "reason": { "value": { "type": "plain_text_input", "value": "Box arrived damaged" } },
        "amount": { "value": { "type": "number_input", "value": "129.5" } }
      }
    }
  }
}
```

This one is **synchronous**: answer within 3 seconds. Your answer decides what happens next.

| Answer | Effect |
|---|---|
| Empty `200` | Close this view (the one below it, if any, shows again) |
| `{ "response_action": "errors", "errors": { "reason": "Too short" } }` | Keep the view open and show each message under its input block |
| `{ "response_action": "update", "view": { … } }` | Replace this view |
| `{ "response_action": "push", "view": { … } }` | Stack a new view on top (up to 3) |
| `{ "response_action": "clear" }` | Close every view in the stack |

If your app doesn't answer in time, the person sees an error and the view stays open.

Files from a [file input](https://workstation.repzo.com/docs/developers/block-kit/elements/file-input-element) arrive as `{ "id", "name", "mimetype", "size", "url_private" }`. Download `url_private` with your bot token within 7 days.

## Update and push from your app

```http
POST /api/bot/views.update
```

```json
{ "view_id": "8d3bc164-…", "hash": "…", "view": { "type": "modal", … } }
```

Pass `external_id` instead of `view_id` if you set one. With `hash`, a view the person changed since you read it fails with `hash_conflict` instead of being overwritten. `views.push` stacks a new view and needs a `trigger_id` from inside the modal.

The person's typed values are kept for inputs whose `block_id` and `action_id` are still there after an update.

## When the person closes it

**Cancel** or the back arrow closes the top view; **×** closes the whole modal. With `notify_on_close`, you get:

```json
{ "type": "view_closed", "view": { … }, "is_cleared": true }
```

`is_cleared` is `true` when the whole stack closed. It is delivered from the queue and needs no answer.

## Errors

| `error` | Meaning |
|---|---|
| `invalid_trigger_id`, `expired_trigger_id`, `exchanged_trigger_id` | Unknown, older than 60 seconds, or already used |
| `push_limit_reached` | Three views are already stacked |
| `not_found` | No such open view for this app |
| `hash_conflict` | The view changed since your `hash` |
| `duplicate_external_id` | Another open view of this app already uses that `external_id` |
| `invalid_arguments` | Missing `view`, wrong `view.type`, or neither `view_id` nor `external_id` |

## Related

- [Input block](https://workstation.repzo.com/docs/developers/block-kit/blocks/input-block)
- [Interactivity](https://workstation.repzo.com/docs/developers/chat-apps/interactivity)
- [App Home](https://workstation.repzo.com/docs/developers/chat-apps/app-home)
