Block Kit or Plain Text? A Pragmatic Guide to Structured Slack Messages
Block Kit gives Slack messages buttons and structure, but a malformed payload fails the whole message. Here's how to decide when structured blocks beat plain text, with a working approval-message example.
03 Feb 2026, 17:05 UTC

Your deploy bot just posted another wall of monospace text into #engineering, and nobody read it. The instinct is to reach for Block Kit — Slack's JSON-based UI framework — and rebuild everything as rich, interactive messages. Sometimes that's exactly right. Sometimes it's overengineering that adds a schema dependency to a message that was fine as a sentence. Here's how to decide.
The thesis: structure pays off when humans must act, not just read
Block Kit structures a message as a JSON array of blocks, each with a type field — section, divider, actions, and so on — plus block-specific properties. Slack renders these natively, so you get buttons, menus, and formatted layouts without writing any client code.
The cost is real, though: you're now generating and validating JSON against a versioned schema, and a malformed payload fails the whole message with an invalid_arguments error from chat.postMessage. Plain text fails gracefully; broken Block Kit fails loudly. So the rule of thumb I use: if the reader needs to do something — approve, acknowledge, drill in — Block Kit earns its complexity. If the message is a notification people skim, a well-formatted text string with a link is cheaper to build and cheaper to maintain.
A worked example: an approval request
Here's a minimal approval message posted via chat.postMessage. You'd send this from your app's backend using a bot token with the chat:write scope, targeting a channel your app has been invited to:
{
"channel": "C0123ABCDEF",
"text": "Deploy approval requested for api-service v2.14.0",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Deploy approval requested*\n`api-service` v2.14.0 → production\nRequested by <@U4567GHIJK>"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "Approve" },
"style": "primary",
"action_id": "deploy_approve",
"value": "api-service:2.14.0"
},
{
"type": "button",
"text": { "type": "plain_text", "text": "Reject" },
"style": "danger",
"action_id": "deploy_reject",
"value": "api-service:2.14.0"
}
]
}
]
}Two details matter here. First, the top-level text field is not optional decoration — it's the fallback shown in notifications and to clients that can't render blocks. Always include a meaningful plain-text summary. Second, the value on each button carries the context your interactivity handler needs, so you don't have to parse it back out of the rendered message.
To verify, post this to a test workspace channel and check two things: the API response contains "ok": true, and the message renders with both buttons. Clicking a button requires your app to expose an interactivity request URL, which is a separate setup step — the message alone won't do anything until that's configured.
Rollout: feature-flag it, because payloads fail
Because a bad Block Kit payload rejects the entire message, a sensible rollout pattern is a per-workspace feature flag: attempt the Block Kit version, and on an invalid_arguments response, fall back to posting the plain-text equivalent. This keeps a schema mistake from becoming a silent notification outage.
Also confirm your app manifest declares an api_version consistent with the block types you use. Slack's schema evolves, and blocks that were fine when you wrote the code can be rejected later — particularly during app review. A periodic audit of your payload builders against Slack's API changelog is unglamorous but necessary maintenance.
The trade-off: size, latency, and maintenance
Slack caps message payloads at 4MB. You won't hit that with an approval button, but dense dashboards — think an incident summary with dozens of sections, context blocks, and select menus — can grow surprisingly fast, and large payloads add latency in busy channels. If you find yourself paginating data into blocks, that's a signal the content belongs behind a link or in a modal loaded on demand, not inline in the channel.
The other cost is a dependency on a schema you don't control. Every Block Kit message in your codebase is a small contract with Slack's platform. For a handful of high-value interactive messages, that's a good deal. For every message your app sends, it's a maintenance surface you'll regret.
Where to start
Pick the one message in your system where a human currently has to leave Slack to take an action. Convert just that one to Block Kit with an actions block, keep the plain-text fallback behind a flag, and verify rendering in a test workspace before shipping. If it saves people a context switch, expand from there. If it doesn't, you've learned that cheaply — and your deploy bot's monospace wall of text can stay exactly as it is.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.