SkillsMay 8, 2026·4 min read

Slack Block Kit Builder — Helper for AI-Generated Messages

Python helper that converts LLM markdown to valid Slack Block Kit JSON. Headers, sections, dividers, buttons. Auto-splits past the 3001-char limit.

Agent ready

Safe staging for this asset

This asset is staged first. The copied prompt tells the agent to inspect the staged files and ask before activating scripts, MCP config, or global config.

Stage only · 29/100Policy: stage
Agent surface
Any MCP/CLI agent
Kind
Skill
Install
Stage only
Trust
Trust: Community
Entrypoint
Asset
Safe staging command
npx -y tokrepo@latest install e50d6b32-9054-4bb8-a230-02546da7f852 --target codex

Stages files first; activation requires review of the staged README and plan.

Intro

This is a small Python helper that takes plain markdown from an LLM and produces valid Slack Block Kit JSON — header, section, divider, fields, buttons, image — automatically chunking text past Slack's 3001-char per-block limit. Slack messages without Block Kit look terrible; messages with Block Kit require fragile hand-built JSON. This bridges them. Best for: AI bots posting summaries, alerts, daily digests, and reports to Slack channels. Works with: Slack Bolt SDK (Python/JS), Slack chat.postMessage REST. Setup time: 5 minutes.


Helper code

from typing import Iterable

def md_to_blocks(text: str) -> list[dict]:
    blocks = []
    for raw in text.split("\n\n"):
        chunk = raw.strip()
        if not chunk:
            continue
        if chunk.startswith("# "):
            blocks.append({"type": "header", "text": {"type": "plain_text", "text": chunk[2:].strip()[:150]}})
        elif chunk == "---":
            blocks.append({"type": "divider"})
        else:
            for piece in _split_3000(chunk):
                blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": piece}})
    return blocks

def _split_3000(s: str, limit: int = 2900) -> Iterable[str]:
    while len(s) > limit:
        cut = s.rfind("\n", 0, limit)
        if cut == -1:
            cut = s.rfind(" ", 0, limit) or limit
        yield s[:cut]
        s = s[cut:].lstrip()
    if s:
        yield s

def add_actions(blocks: list[dict], buttons: list[tuple[str, str, str]]) -> list[dict]:
    if not buttons:
        return blocks
    blocks.append({
        "type": "actions",
        "elements": [
            {"type": "button", "text": {"type": "plain_text", "text": label},
             "action_id": action_id, "url": url}
            for label, action_id, url in buttons
        ],
    })
    return blocks

Usage with Bolt

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import os

app = App(token=os.environ["SLACK_BOT_TOKEN"])
llm_text = '''# Daily AI Digest

*5 new MCP servers* shipped on TokRepo today.

- Stripe MCP — payments
- Datadog MCP — observability
- Linear MCP — issue tracker

---

Top reaction: stripe-mcp got 47 stars in 6 hours.'''

blocks = md_to_blocks(llm_text)
blocks = add_actions(blocks, [("Open TokRepo", "open_tokrepo", "https://tokrepo.com/explore")])

app.client.chat_postMessage(channel="#ai-digest", blocks=blocks, text="Daily AI Digest")

Slack Block Kit limits (cheat sheet)

Item Limit
Blocks per message 50
Section text 3,000 chars
Header text 150 chars
Action elements per actions block 25
Total message size 40 KB

FAQ

Q: Why mrkdwn instead of markdown? A: Slack's mrkdwn is a subset — *bold* (one asterisk, not two), _italic_, <url|label> for links, no headings, no tables. The helper outputs mrkdwn-flavored text. Hand-converting LLM markdown is the part this saves.

Q: Does the LLM need to know about Block Kit? A: No — that's the point. Prompt it for normal markdown. The helper handles the conversion. Hand the LLM Block Kit JSON in the prompt and outputs become brittle and verbose.

Q: What if the message is over 50 blocks? A: Slack rejects with invalid_blocks error. Send as a thread — first message gets the first 50 blocks, then chat.postMessage with thread_ts for follow-ups. Or split into multiple top-level messages with day/section headers.


Quick Use

  1. pip install slack-bolt
  2. Drop md_to_blocks() into your bot module
  3. Pipe LLM markdown straight into chat.postMessage(blocks=md_to_blocks(text))

Intro

This is a small Python helper that takes plain markdown from an LLM and produces valid Slack Block Kit JSON — header, section, divider, fields, buttons, image — automatically chunking text past Slack's 3001-char per-block limit. Slack messages without Block Kit look terrible; messages with Block Kit require fragile hand-built JSON. This bridges them. Best for: AI bots posting summaries, alerts, daily digests, and reports to Slack channels. Works with: Slack Bolt SDK (Python/JS), Slack chat.postMessage REST. Setup time: 5 minutes.


Helper code

from typing import Iterable

def md_to_blocks(text: str) -> list[dict]:
    blocks = []
    for raw in text.split("\n\n"):
        chunk = raw.strip()
        if not chunk:
            continue
        if chunk.startswith("# "):
            blocks.append({"type": "header", "text": {"type": "plain_text", "text": chunk[2:].strip()[:150]}})
        elif chunk == "---":
            blocks.append({"type": "divider"})
        else:
            for piece in _split_3000(chunk):
                blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": piece}})
    return blocks

def _split_3000(s: str, limit: int = 2900) -> Iterable[str]:
    while len(s) > limit:
        cut = s.rfind("\n", 0, limit)
        if cut == -1:
            cut = s.rfind(" ", 0, limit) or limit
        yield s[:cut]
        s = s[cut:].lstrip()
    if s:
        yield s

def add_actions(blocks: list[dict], buttons: list[tuple[str, str, str]]) -> list[dict]:
    if not buttons:
        return blocks
    blocks.append({
        "type": "actions",
        "elements": [
            {"type": "button", "text": {"type": "plain_text", "text": label},
             "action_id": action_id, "url": url}
            for label, action_id, url in buttons
        ],
    })
    return blocks

Usage with Bolt

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import os

app = App(token=os.environ["SLACK_BOT_TOKEN"])
llm_text = '''# Daily AI Digest

*5 new MCP servers* shipped on TokRepo today.

- Stripe MCP — payments
- Datadog MCP — observability
- Linear MCP — issue tracker

---

Top reaction: stripe-mcp got 47 stars in 6 hours.'''

blocks = md_to_blocks(llm_text)
blocks = add_actions(blocks, [("Open TokRepo", "open_tokrepo", "https://tokrepo.com/explore")])

app.client.chat_postMessage(channel="#ai-digest", blocks=blocks, text="Daily AI Digest")

Slack Block Kit limits (cheat sheet)

Item Limit
Blocks per message 50
Section text 3,000 chars
Header text 150 chars
Action elements per actions block 25
Total message size 40 KB

FAQ

Q: Why mrkdwn instead of markdown? A: Slack's mrkdwn is a subset — *bold* (one asterisk, not two), _italic_, <url|label> for links, no headings, no tables. The helper outputs mrkdwn-flavored text. Hand-converting LLM markdown is the part this saves.

Q: Does the LLM need to know about Block Kit? A: No — that's the point. Prompt it for normal markdown. The helper handles the conversion. Hand the LLM Block Kit JSON in the prompt and outputs become brittle and verbose.

Q: What if the message is over 50 blocks? A: Slack rejects with invalid_blocks error. Send as a thread — first message gets the first 50 blocks, then chat.postMessage with thread_ts for follow-ups. Or split into multiple top-level messages with day/section headers.


Source & Thanks

Pattern compiled from Slack Block Kit + Bolt SDK.

Bolt MIT-licensed, helper Apache-2.0.

🙏

Source & Thanks

Pattern compiled from Slack Block Kit + Bolt SDK.

Bolt MIT-licensed, helper Apache-2.0.

Discussion

Sign in to join the discussion.
No comments yet. Be the first to share your thoughts.

Related Assets