Slack Buttons That Actually Do Something: Block Kit Interactivity as an Engineering Decision
Slack Block Kit turns passive alerts into actionable messages. The catch: verify every interaction payload's HMAC signature and ack within 3 seconds, or your buttons become a liability.
04 Jul 2026, 10:22 UTC

Your CI pipeline posts \"Deploy failed on staging\" to a channel. Everyone reads it. Nobody acts on it, because acting means opening three other tabs. The useful takeaway: Slack's Block Kit lets you put a working button inside that message — but only if you respect the two hard rules of the interaction contract: verify every payload's signature, and acknowledge within three seconds.
The decision: text notification or actionable message
Block Kit is Slack's supported JSON framework for composing rich messages — sections, dividers, buttons, select menus — and it has replaced legacy message attachments as the recommended approach. The engineering decision isn't really about prettier messages. It's about whether a notification is the end of a workflow or a step in one.
A plain-text alert is fine when the reader only needs awareness. The moment the reader needs to approve, acknowledge, assign, or retry, an interactive message collapses a multi-app detour into one click — and the click lands back in your system as a structured event with the user's identity attached. That's the actual payoff: an auditable action, not a nicer layout.
How the interaction contract works
Interactivity is a two-way contract:
- Your app posts a message containing interactive elements, each with an
action_id. - When a user clicks, Slack POSTs an interaction payload to your app's configured Request URL (or delivers it over Socket Mode, a persistent WebSocket useful for development and apps behind firewalls).
- Your app must respond with HTTP 200 within 3 seconds. Anything slower — database writes, downstream API calls — belongs in a background job, with results posted later via the payload's
response_urlor achat.updatecall.
That 3-second budget is the constraint that shapes your architecture. Treat the webhook handler like a queue producer: verify, enqueue, ack. Teams that do the work inline discover the timeout the first time their database hiccups.
Worked example: a deploy approval button
A deploy-request message with Approve and Reject buttons, posted with chat.postMessage (run from your app's backend using a bot token with chat:write scope):
{\n \"channel\": \"C0123DEPLOY\",\n \"text\": \"Deploy request: billing-service v2.14.0\",\n \"blocks\": [\n {\n \"type\": \"section\",\n \"text\": {\"type\": \"mrkdwn\", \"text\": \"*Deploy request*\\n`billing-service` v2.14.0 → production\"}\n },\n {\n \"type\": \"actions\",\n \"elements\": [\n {\"type\": \"button\", \"action_id\": \"deploy_approve\", \"style\": \"primary\",\n \"text\": {\"type\": \"plain_text\", \"text\": \"Approve\"},\n \"value\": \"deploy-4821\"},\n {\"type\": \"button\", \"action_id\": \"deploy_reject\", \"style\": \"danger\",\n \"text\": {\"type\": \"plain_text\", \"text\": \"Reject\"},\n \"value\": \"deploy-4821\"}\n ]\n }\n ]\n}The handler (this is where the security lives) must:
- Verify authenticity. Recompute HMAC-SHA256 over
v0:timestamp:raw_bodyusing your app's signing secret and compare it to theX-Slack-Signatureheader. Reject requests with timestamps older than about 5 minutes to limit replay. Skipping this means anyone who finds your endpoint can forge approvals. - Ack immediately with HTTP 200, then hand the real work (triggering the deploy, writing the audit record) to a background job.
- Update the message in place via
chat.updateso the buttons are replaced with \"Approved by @priya\" — the payload'suserfield gives you the actor for free, and updating prevents double-clicks from double-deploying.
Slack's Bolt framework (JavaScript, Python, Java) handles signature verification, ack timing, and listener registration for you. Unless you have a specific reason, use Bolt rather than hand-rolling the HTTP plumbing — the failure modes of a hand-rolled verifier are exactly the ones you don't want to discover in production.
Trade-offs and limitations
- Delivery dependency. Interactivity requires Slack to reach your endpoint, or a healthy Socket Mode connection. Socket Mode simplifies local development but adds a persistent WebSocket dependency that isn't always right for high-scale production apps.
- Payload limits. Message JSON has block-count and size limits, modals have view constraints, and
chat.update/chat.postMessageare rate-limited. These numbers are API-version-sensitive — check the current Block Kit reference and changelog before baking assumptions into code. - Verbose JSON. Deeply nested layouts can fail validation only at post time. Prototype in Slack's Block Kit Builder tool before writing generator code.
- Dead buttons. Buttons in ephemeral or expired messages can silently stop working; prefer updating the original message over posting follow-ups that reference stale state.
Try it in an afternoon
Create a free app in the Slack API console, enable Interactivity, and post the example message above. Then run three checks: log the raw headers and recompute the HMAC yourself to confirm your verifier matches; add an artificial 5-second delay to watch the timeout behavior, then refactor to ack-plus-async; and click a button twice fast to confirm your chat.update actually prevents a double action. If all three pass, you have the pattern that scales from one approval button to a full incident-response workflow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.