Designing an incident intake flow with modal_body
Compose a modal-driven 'Report incident' button, validate its blocks, and forward the submitted answers to your own server or straight into a workflow — full modal_body contract.
An incident intake button that just says “Report incident” and posts a blank message to a channel loses the one thing that matters most: structure. modal_body turns that button into a small form — 1 to 10 fields, opened inline, forwarded wherever your triage process lives.
The button and its modal
Attach modal_body to an interactive button. The modal opens on click, before any callback fires:
curl -sS -X POST 'https://api.dailybot.com/v1/send-message/' \
-H 'X-API-KEY: $DAILYBOT_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"message": "Something wrong? Report it here.",
"target_channels": ["C0123456789"],
"buttons": [
{"label": "Report incident", "button_type": "interactive", "value": "report_incident",
"callback_url": "https://ops.example.com/hooks/incident-intake",
"modal_body": {
"title": "New incident",
"submit_label": "Submit",
"blocks": [
{"type": "text", "text": "Give us the essentials — triage picks it up from here."},
{"type": "input", "name": "summary", "label": "What happened?", "multiline": true, "required": true},
{"type": "input", "name": "severity", "label": "Severity (1-4)", "required": true},
{"type": "divider"}
]
}}
]
}'
modal_body — the full contract
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string, ≤ 200 chars | yes | Modal title |
submit_label |
string, ≤ 200 chars | no | Default "Submit" |
blocks |
array, 1–10 items | yes | See block types below |
Only three block types exist — nothing else validates:
type |
Purpose | Key fields |
|---|---|---|
text |
Display-only copy | text (≤ 3000 chars) |
input |
A labeled field | name, label, multiline?, required?, placeholder?, max_length? (≤ 3000), default? |
divider |
Visual separator | — |
input.name must match ^[a-z][a-z0-9_]{0,62}$ and be unique within the modal — this is the key every submitted value comes back under. The whole modal_body, serialized, must be ≤ 8 KiB. Slack renders the modal natively; other platforms collect the same fields conversationally (one prompt per field, in block order).
Requiring a callback — or not
A modal with any input block requires callback_url or callback_workflow on the same button — Dailybot needs somewhere to deliver the answers. Omit both with an input present and you get 400 button_modal_body_invalid, detail input_without_callback. A display-only modal (only text and divider blocks, no input) needs neither: clicking Submit just closes it.
Other validation failures collapse to the same button_modal_body_invalid code: an unsupported block type, 11 or more blocks, or a missing/duplicate name.
Forwarding to your own server (callback_url)
On submit, Dailybot POSTs to callback_url with event: "modal_submit" and the answers under modal_fields:
{
"event": "modal_submit",
"bot_message_id": "$db/...",
"button": {"id": "$btn/...", "value": "report_incident"},
"user": {"id": "...", "email": "...", "display_name": "...", "external_id": "U..."},
"organization": {"id": "...", "name": "..."},
"platform": "slack",
"channel": {"id": "C0123456789", "type": "channel"},
"modal_fields": {
"summary": "Checkout API returning 500s intermittently since 14:05 UTC.",
"severity": "2"
},
"metadata": {},
"sent_at": "2026-07-22T14:30:00Z",
"clicked_at": "2026-07-22T14:31:12Z"
}
This carries the same X-Dailybot-Signature HMAC header as any other callback_url dispatch — verify it before trusting modal_fields, exactly as you would a plain button click (see the CI/CD approval gates deep-dive for the full verification snippet). Your intake service reads modal_fields.summary and modal_fields.severity, opens a ticket, and can reply asynchronously by editing the original message (reuse bot_message_id within 72 hours) or sending a fresh threaded update.
Forwarding straight into a workflow (callback_workflow)
Skip the external server entirely and route submissions into an api_trigger workflow instead — the field values arrive as {{trigger.fields.<name>}}:
{"label": "Report incident", "button_type": "interactive", "value": "report_incident",
"callback_workflow": "$INCIDENT_TRIAGE_WORKFLOW_UUID",
"modal_body": {
"title": "New incident",
"blocks": [
{"type": "input", "name": "summary", "label": "What happened?", "multiline": true, "required": true},
{"type": "input", "name": "severity", "label": "Severity (1-4)", "required": true}
]
}}
Inside the workflow’s steps, {{trigger.fields.summary}} and {{trigger.fields.severity}} are available to compose a follow-up message, pre-fill a form answer, or feed an AI triage prompt — no signing secret involved, since the trigger stays entirely internal to Dailybot.
Choosing callback_url vs. callback_workflow for intake
callback_url |
callback_workflow |
|
|---|---|---|
| Where triage logic lives | Your own service | Inside a Dailybot api_trigger workflow |
| Needs signature verification | Yes — always | No — internal, no signing |
| Best for | Existing ticketing systems, PagerDuty-style integrations | Keeping the whole flow inside Dailybot (route to a channel, open a form, notify an on-call team) |
Both are mutually exclusive with each other and with callback_form / callback_command / callback_prompt — a button carries exactly one.
CLI: full JSON pass-through
The ergonomic button flags (--approve-button, --workflow-button) don’t express a modal — use --buttons for the full contract:
dailybot chat send -c C0123456789 -m "Something wrong? Report it here." \
--buttons '[{"label":"Report incident","button_type":"interactive","value":"report_incident",
"callback_url":"https://ops.example.com/hooks/incident-intake",
"modal_body":{"title":"New incident","submit_label":"Submit",
"blocks":[
{"type":"text","text":"Give us the essentials — triage picks it up from here."},
{"type":"input","name":"summary","label":"What happened?","multiline":true,"required":true},
{"type":"input","name":"severity","label":"Severity (1-4)","required":true}
]}}]'
Cross-references
- The send-message API, end to end for the full button contract
modal_bodyattaches to. - Triggering workflows from a chat button for the
callback_workflowpath in depth. /developers/api/messaging#send-messagefor the structuredmodal_bodyfield reference.
Browse the Solutions hub for the product-facing tour of a modal-driven incident intake flow.
FAQ
- What block types can a modal_body use?
- Exactly three: text (display-only copy), input (a labeled field, optionally multiline and required), and divider. 1 to 10 blocks total, and the serialized modal_body must be 8 KiB or smaller.
- Where do submitted modal answers end up?
- As modal_fields.<name> in the callback_url POST body (alongside event: "modal_submit"), or as {{trigger.fields.<name>}} inside a triggered workflow's steps when the button used callback_workflow instead.
- Can a modal exist without any callback?
- Only if it has no input blocks — a display-only modal (just text and divider blocks) needs neither callback_url nor callback_workflow; clicking Submit just closes it. A modal with any input block and no callback returns 400 button_modal_body_invalid with detail input_without_callback.
- What are the naming rules for an input block?
- name must match ^[a-z][a-z0-9_]{0,62}$ and be unique within the modal. Violating that, having 11+ blocks, an unsupported block type, or a missing/duplicate name all return 400 button_modal_body_invalid.
- Can I combine an internal form with a modal for the same intake flow?
- Not on the same button — callback_form and modal_body serve different jobs. callback_form opens a full Dailybot form (its own lifecycle, workflow states, and report channels); modal_body is a lightweight, 1-to-10-field capture attached directly to a button and forwarded to callback_url or callback_workflow. Pick modal_body for a quick structured capture, and the forms deep-dive's callback_form pattern when you need the full form lifecycle.