Sangriadocs

Message components

Attach mrkdwn text and buttons to a message an app posts, and receive a signed callback when a button is clicked.

An app posting via the incoming webhook or bot API can attach blocks to its message: rich section text, a row of actions buttons, a divider, an image, a header, or a context line. Clicking a button fires a signed interaction event to the app's event URL — the same delivery pipeline outgoing events already use, just a new event type. Button delivery is fire-and-forget: the click can't rewrite the original message as part of its response. React to it asynchronously via the bot API instead. These six block types are what's supported — select menus, checkboxes, and other input elements, input/modal blocks, table/data_table, video, and full rich_text blocks are not (see Scope below).

The blocks field

Add blocks alongside text when posting — a message can mix section and actions blocks, in the order they should render:

{
  "text": "Approve this deploy?",
  "blocks": [
    {
      "type": "section",
      "text": "*Deploy #4213* is ready. Triggered by `ci-bot` on `main`. See the <https://example.com/pr/42|full diff>."
    },
    {
      "type": "actions",
      "elements": [
        { "type": "button", "text": "Approve", "actionId": "approve", "style": "primary" },
        { "type": "button", "text": "Reject", "actionId": "reject", "style": "danger" },
        { "type": "button", "text": "View diff", "url": "https://example.com/pr/42" }
      ]
    }
  ]
}
LimitValue
Blocks per message10
Section text length3000 characters
Buttons per actions block5
Button text length75 characters
Header text length150 characters
Elements per context block10
Image alt text length2000 characters

A malformed or oversized payload is rejected the same way an unrecognized text shape is — see the status codes on the incoming webhook and bot API page.

Section blocks

A section block is a chunk of mrkdwn-formatted text — Slack's own formatting syntax, not standard CommonMark:

SyntaxRenders as
*bold*bold
_italic_italic
~strike~strike
`code`code
<https://example.com|label>a link reading "label"
<https://example.com>a link reading the URL itself

Formatting is single-pass and doesn't nest (*bold _and italic_* isn't specially handled — Slack's own mrkdwn doesn't nest cleanly either). Only http/https links render as clickable — anything else (javascript:, data:, etc.) renders as inert plain text.

Actions blocks

Each button is exactly one of two kinds:

  • actionId — a callback button. Clicking it fires an interaction event back to your app with this id, so you know which button was pressed.
  • url — a plain link. It opens directly; no event fires.

A button needs exactly one of actionId or url, never both, never neither. style is optional (primary, danger, or omit it for the neutral default).

Divider blocks

A plain horizontal rule — no fields:

{ "type": "divider" }

Image blocks

{
  "type": "image",
  "imageUrl": "https://example.com/screenshot.png",
  "altText": "Deploy dashboard screenshot",
  "title": "Latest deploy"
}

imageUrl must be http/https — unlike an unsafe link inside a section block (which just renders as inert plain text), an unsafe imageUrl rejects the entire message with a 400. Slack's own field names also work: image_url, alt_text, and a { "type": "plain_text", "text": "…" }-wrapped title.

Header blocks

{ "type": "header", "text": "Deploy #4213" }

Plain text only — unlike section, header text is never run through the mrkdwn tokenizer (this matches Slack's own header block, which is always plain_text). Slack's { "type": "plain_text", "text": "…" } wrapper is also accepted and normalized to a bare string.

Context blocks

A small row of muted text and/or inline images, useful for a byline or timestamp:

{
  "type": "context",
  "elements": [
    { "type": "text", "text": "Posted by `ci-bot`" },
    { "type": "image", "imageUrl": "https://example.com/avatar.png", "altText": "ci-bot" }
  ]
}

Text elements are mrkdwn-formatted, same as a section block. Slack's own element shapes also work: a mrkdwn or plain_text typed element normalizes to text, and image_url/alt_text normalize the same way the image block's do.

Scope

Supported block types

section, actions, divider, image, header, and context — that's the full set. Select menus, checkboxes, and other interactive elements beyond a plain button; input/modal blocks; table/data_table; video; and full rich_text blocks are all out of scope — a message using one of these is rejected with a clean 400, not silently dropped or ignored. Button delivery is fire-and-forget: your app can't rewrite the message in the interaction response. Post a follow-up message via the bot API instead if you need to reflect the outcome.

The interaction event

Subscribe your app to the interaction event type the same way you'd subscribe to message or reaction (setEventSubscription, or the Events tab in the workspace's integrations settings). It's delivered only to the one app that owns the clicked message's blocks — not fanned out to every app installed in the channel the way message/reaction events are.

{
  "id": "interaction:<messageId>:<memberId>:<actionId>:<timestamp>",
  "type": "interaction",
  "timestamp": 1731000000000,
  "workspace": { "id": "<workspaceId>" },
  "channel": { "id": "<channelId>", "name": "general" },
  "user": { "memberId": "<memberId>", "name": "Ada" },
  "message": { "id": "<messageId>" },
  "actionId": "approve"
}

Signing, headers (X-Webhook-Timestamp / X-Webhook-Signature), and SSRF protection are identical to every other outgoing event — see Outgoing event subscriptions for the full verification walkthrough. Don't re-derive it here; the HMAC construction is the same regardless of event type.

Try it: block builder

Write some mrkdwn text, pick some buttons, and copy the payload straight into a webhook/API call. The builder covers section and actions — for divider/image/header/context, copy the JSON examples above directly.

Button 1
Preview

Approve this deploy?

Deploy #4213 is ready. Triggered by ci-bot on main. See the full diff.

Button 1
Payload
{
  "text": "Approve this deploy?",
  "blocks": [
    {
      "type": "section",
      "text": "*Deploy #4213* is ready. Triggered by `ci-bot` on `main`. See the <https://example.com/pr/42|full diff>."
    },
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": "Button 1",
          "actionId": "action_1"
        }
      ]
    }
  ]
}

On this page