Choosing Between Plain Text and Block Kit for Slack chat.postMessage
A decision guide that compares plain text and Block Kit options for Slack messages, outlines trade‑offs, and shows a concrete implementation with validation steps.
05 Sept 2026, 20:16 UTC

Decision Overview
When building a Slack integration that sends messages via the chat.postMessage API, you must decide whether to send a plain‑text payload or to use Block Kit for richer formatting and interactivity. The decision hinges on the need for visual layout, interactive components, and threading behavior while staying within Slack’s rate limits and response‑time constraints.
Constraints
- Rate limit: Up to 1 message per second per workspace; exceeding returns HTTP 429.
- Interactive acknowledgment: Any button, menu, or datepicker must be acknowledged with a 200 OK within 3 seconds, otherwise Slack shows a timeout.
- Required scopes:
chat:writefor posting messages;commandsif the message is triggered by a slash command. - Message size: Block Kit JSON must be under 30 KB.
Option Comparison
| Option | Supported Features | Pros | Cons |
|---|---|---|---|
| Plain text | Simple string, optional thread_ts |
Minimal payload, fastest to send, no schema validation errors | No formatting, no buttons, no images, limited visual hierarchy |
| Block Kit – Section only | Rich text (mrkdwn), optional fields, thread_ts |
Bold/italic, lists, links, emojis; still lightweight | No interactive elements; still requires JSON validation |
| Block Kit – Section + Actions | Section blocks plus actions block with buttons, menus, datepickers |
Interactive UI, call‑to‑action, can collect user input | Larger payload, must handle interactive payloads, stricter 3‑second ack |
| Block Kit – Full layout (section, context, divider, image) | All block types for complex cards | Maximum visual control, can include images and context | Highest payload size, more validation points, higher chance of schema errors |
Trade‑offs
If your message only needs to convey information and you want the lowest latency and simplest error handling, plain text is sufficient. Adding a Section block gives you basic formatting without the overhead of interactivity. When you need users to act—such as approving a request, launching a modal, or selecting a date—you must include an Actions block, which introduces the responsibility of acknowledging interactive payloads quickly. Full layouts are justified for dashboards or rich notifications but increase the risk of hitting the 30 KB limit and require more careful JSON construction.
Concrete Implementation: Posting a Section with an Action Button
The following example shows how to post a message that contains a section with markdown text and a button that triggers a modal. Replace placeholders with your own values.
Where to run
Run this code on your backend service that has been granted the chat:write and commands OAuth scopes for the bot user.
Required permissions
- The bot token (starting with
xoxb-) must have thechat:writescope. - If the message is sent in response to a slash command, also include
commands.
Example payload (Node.js)
const axios = require('axios');
async function postSlackMessage(botToken, channelId, threadTs) {
const payload = {
channel: channelId,
thread_ts: threadTs || undefined, // omit if not threading
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*New deployment ready*\nPlease review and approve.'
}
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'Approve' },
style: 'primary',
value: 'approve_deployment',
action_id: 'approve_button'
}
]
}
]
};
try {
const response = await axios.post('https://slack.com/api/chat.postMessage', payload, {
headers: { Authorization: `Bearer ${botToken}`, 'Content-Type': 'application/json' }
});
if (!response.data.ok) {
throw new Error(`Slack API error: ${response.data.error}`);
}
console.log('Message posted, ts:', response.data.ts);
return response.data.ts;
} catch (err) {
console.error('Failed to post message:', err.message);
throw err;
}
}
module.exports = { postSlackMessage };
Expected checks
- HTTP response status 200.
- JSON body contains
"ok": true. - The
tsfield is a string representing the message timestamp. - If
thread_tswas supplied, the returnedthread_tsmatches the input.
Risks
- Exceeding the 1‑msg/sec limit yields error 429; implement retry with back‑off.
- If the button is clicked, your interactive endpoint must respond with 200 OK within 3 seconds; otherwise Slack shows a timeout to the user.
Validation Steps
- Call
postSlackMessagewith a valid bot token, channel ID, and optionalthread_ts. - Verify the response meets the expected checks above.
- To test interactivity, click the Approve button in Slack.
- Ensure your configured interactivity endpoint receives a payload of type
block_actionscontaining: "type":"block_actions""actions":[{ "action_id":"approve_button", "value":"approve_deployment" }]"container":{ "message_ts":"" }- Respond with HTTP 200 and an empty body (or a
response_actionif you want to update the message). - Observe that Slack does not display a timeout error.
Limitations and Practical Checks
Even with a valid payload, Slack may return:
invalid_blocksif any block fails schema validation—inspect theerrorfield for details.rate_limited(HTTP 429) when exceeding the per‑workspace limit—check theRetry-Afterheader and delay subsequent calls.channel_not_foundornot_in_channelif the bot lacks membership—invite the bot to the channel first.
To confirm the message is threaded correctly, retrieve the message via conversations.history using the returned ts and verify that thread_ts matches the parent timestamp (or is absent for a top‑level message).
By following this decision guide, you can select the appropriate message format for your Slack integration, implement a reliable Block Kit payload with interactive elements, and validate both the posting and interaction flows within Slack’s constraints.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.