Skip to content
Academy Menu

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.

deep-diveDeveloperOps9 min read

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"}
         ]
       }}
    ]
  }'
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

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.