Write Markdown, get Slack Block Kit.
slackmd converts Markdown into Slack Block Kit JSON you can drop straight into chat.postMessage's blocks field.
It is inspired by emailmd — where emailmd turns Markdown into email-safe HTML, slackmd turns it into Slack-safe Block Kit. Write in a format you already know; get clean output for a fussy rendering surface. If you've used emailmd, slackmd should feel immediately familiar.
Slack has a native markdown block — so why a tool? Because it falls short exactly where rich messages need it:
Raw markdown block |
slackmd | |
|---|---|---|
| Images | Degraded to link text | Real image blocks |
| Headings | All levels render identically | header blocks (visual hierarchy) |
| Buttons / footers | Not possible | actions / context via directives |
| Tables | Best-effort text | Native table blocks |
| Output | Opaque string | Diffable, testable JSON |
| Problems | Silent | Surfaced in a warnings[] array |
slackmd compiles Markdown to fully structured Block Kit by default — and can still emit a native markdown block when you want one (see Rendering engines).
npm install @heumsi/slackmdRequires Node.js ≥ 20. The package is pure ESM.
Library
import { slackmd } from "@heumsi/slackmd";
const { blocks, warnings } = slackmd(`
# Deployment complete
The service is now running in **us-east-1**.
- Version: 2.3.1
- Duration: 45s
`);
await client.chat.postMessage({ channel: "#deploys", blocks });slackmd is a pure function and never throws. Anything that can't map cleanly to Block Kit is downgraded gracefully and reported in warnings — see Warnings and limits.
CLI
slackmd notes.md # → { "blocks": [...] } on stdout
cat notes.md | slackmd --minify # compact, reading from stdinFull flags are in the CLI reference.
Input
---
engine: rich_text
---
# Incident: DB Latency Spike
:::callout{level=warning}
Elevated latency detected on `db-primary`. Reads degraded, writes unaffected.
:::
## Timeline
- **14:02** — alert fired
- **14:08** — root cause identified (slow query, no index)
- **14:23** — hotfix deployed, latency normal
:::buttons
::button[View Runbook]{url=https://wiki.example.com/runbook style=primary}
::button[View Metrics]{url=https://metrics.example.com}
:::
:::context
Reported by PagerDuty · auto-posted by slackmd
:::Output (abbreviated)
{
"blocks": [
{ "type": "header", "text": { "type": "plain_text", "text": "Incident: DB Latency Spike", "emoji": true } },
{ "type": "rich_text", "elements": [{ "type": "rich_text_quote", "elements": [{ "type": "text", "text": "⚠️ " }, { "type": "text", "text": "Elevated latency detected on " }, { "type": "text", "text": "db-primary", "style": { "code": true } }, { "type": "text", "text": ". Reads degraded, writes unaffected." }] }] },
{ "type": "header", "text": { "type": "plain_text", "text": "Timeline", "emoji": true } },
{ "type": "rich_text", "elements": [{ "type": "rich_text_list", "style": "bullet", "indent": 0, "elements": ["..."] }] },
{ "type": "actions", "elements": [{ "type": "button", "text": { "type": "plain_text", "text": "View Runbook", "emoji": true }, "url": "https://wiki.example.com/runbook", "style": "primary" }, { "type": "button", "text": { "type": "plain_text", "text": "View Metrics", "emoji": true }, "url": "https://metrics.example.com" }] },
{ "type": "context", "elements": [{ "type": "mrkdwn", "text": "Reported by PagerDuty · auto-posted by slackmd" }] }
]
}slackmd targets the message surface (chat.postMessage) and emits only message-safe blocks.
| Markdown | Block Kit output |
|---|---|
# H1, ## H2 |
header block (plain_text, up to 150 chars) |
### H3 … ###### H6 |
bold rich_text_section |
Paragraph, bold, italic, `code`, links |
rich_text block |
:emoji_name: |
emoji element inside rich_text |
- item, * item |
rich_text_list (bullet) |
1. item |
rich_text_list (ordered) |
- nested |
rich_text_list with indent |
- [ ] task, - [x] done |
rich_text_list with ☐/☑ (or emoji) |
> blockquote |
rich_text_quote |
```lang … ``` |
rich_text_preformatted |
--- |
divider block |
 |
image block |
| GFM table | table block (column alignment preserved) |
| Raw HTML | stripped (HTML_STRIPPED warning) |
:::buttons :::context :::fields :::callout |
see Directives |
Set the engine via the engine option (or --engine on the CLI).
Parses Markdown and emits fully structured Block Kit — header, rich_text, table, image, divider, and directive blocks. Full control over the output, broad compatibility across all bot types. Recommended.
Passes prose through Slack's native markdown block (one Slack-flavored Markdown string), intercepting only what that block can't do well: images become image blocks and directives become their own blocks.
Caveat: Slack documents the
markdownblock as tied to AI-feature apps, so its availability for a plain bot token isn't guaranteed. Preferrich_textwhen in doubt.
Slack-native components that Markdown has no syntax for, written with remark-directive syntax. Directives work in both engines.
:::buttons
::button[View docs]{url=https://example.com style=primary}
::button[Cancel]{style=danger}
::button[Learn more]{url=https://example.com/learn value=learn}
:::Each ::button supports url (link to open), style (primary or danger; omit for default), and value (payload for your action handler). An empty :::buttons is skipped with an EMPTY_BUTTONS warning.
:::context
Posted by **DeployBot** · Region: us-east-1
:::Small, muted text (with optional images) — ideal for footers, captions, and metadata.
:::fields
- Status: Active
- Region: us-east-1
- Owner: alice
:::Each list item becomes a field, rendered in a two-column grid. An empty :::fields is skipped with an EMPTY_FIELDS warning.
:::callout{level=warning}
Avoid deploying during peak hours.
:::| Level | Emoji | Use for |
|---|---|---|
info |
ℹ️ | Informational messages |
warning |
Caution / potential issue | |
error |
⛔ | Errors / failures |
success |
✅ | Confirmations |
note (default) |
📝 | Notes / reminders |
Slack's
alertblock is modal-only, so callouts render asrich_text_quoteblocks to display in messages.
The same three options can be passed to the library, set in a document's frontmatter, or (on the CLI) given as flags. Precedence: caller option > frontmatter > default.
| Option | Type | Default | Description |
|---|---|---|---|
engine |
rich_text | markdown |
rich_text |
Rendering engine (see above) |
headerLevel |
1–6 |
2 |
Highest heading level that becomes a header block; deeper headings become bold text |
taskList |
unicode | emoji |
unicode |
Task checkbox style: ☐/☑ or :white_large_square:/:white_check_mark: |
Frontmatter — set defaults for a single document:
---
engine: rich_text
headerLevel: 2
taskList: unicode
---
# My documentUnknown keys or invalid values are ignored with a warning (UNKNOWN_FRONTMATTER_KEY / INVALID_FRONTMATTER_VALUE).
Headings — Markdown has six heading levels; Slack's header block has a single size and holds plain text only. So levels up to headerLevel become header blocks and the rest become bold rich_text. A heading longer than 150 characters or containing inline formatting also falls back to bold rich_text — no text is lost (HEADER_TRUNCATED / HEADER_FALLBACK_BOLD).
slackmd [input] [options]
| Flag | Default | Description |
|---|---|---|
[input] |
stdin | Markdown file path; reads stdin if omitted |
-o, --out <file> |
stdout | Write output to a file |
-e, --engine <engine> |
rich_text |
rich_text or markdown |
--header-level <n> |
2 |
Max heading level for header blocks |
--task-list <style> |
unicode |
unicode or emoji task checkboxes |
--wrap <mode> |
message |
message → { "blocks": [...] }; blocks → bare array |
--minify |
off | Compact JSON (default output is 2-space pretty-printed) |
# Pipe from stdin, emit a bare array for the Slack API `blocks` field
cat release.md | slackmd --wrap blocks
# markdown engine, written to a file
slackmd release.md --engine markdown -o payload.json
# H1–H3 as header blocks, emoji task checkboxes
slackmd notes.md --header-level 3 --task-list emojiimport { slackmd } from "@heumsi/slackmd";
import type {
Block, Engine, SlackmdOptions, SlackmdResult, TaskListStyle, Warning,
} from "@heumsi/slackmd";
function slackmd(md: string, options?: SlackmdOptions): SlackmdResult;interface SlackmdOptions {
engine?: "rich_text" | "markdown"; // default: "rich_text"
headerLevel?: number; // 1–6, default: 2
taskList?: "unicode" | "emoji"; // default: "unicode"
}
interface SlackmdResult {
blocks: Block[]; // ready for chat.postMessage({ channel, blocks })
warnings: Warning[]; // never throws — problems are reported here
}
interface Warning {
code: string;
message: string;
line?: number;
}slackmd never throws. Whatever can't be represented exactly in Block Kit is handled gracefully and reported in warnings (Array<{ code, message, line? }>). The limits below are Slack platform constraints, not slackmd restrictions.
| Code | Meaning |
|---|---|
HEADER_TRUNCATED |
Heading over 150 chars → rendered as bold rich_text (no text lost) |
HEADER_FALLBACK_BOLD |
Heading with inline formatting → rendered as bold rich_text |
QUOTE_FLATTENED |
Block content inside a blockquote was flattened to text |
LIST_ITEM_FLATTENED |
Non-paragraph content in a list item was dropped |
TABLE_ROWS_TRUNCATED |
Table exceeded 100 rows; extra rows dropped |
TABLE_COLS_TRUNCATED |
Table exceeded 20 columns; extra cells dropped |
HTML_STRIPPED |
Raw HTML was removed |
BLOCK_LIMIT_EXCEEDED |
Over 50 blocks (Slack's per-message cap); output is unchanged — split across messages |
MARKDOWN_BLOCK_CHAR_LIMIT |
markdown engine content exceeded 12,000 chars; split into multiple blocks |
EMPTY_BUTTONS |
:::buttons had no ::button items; block skipped |
EMPTY_FIELDS |
:::fields had no items; block skipped |
UNKNOWN_DIRECTIVE |
Unrecognized :::directive name |
UNKNOWN_FRONTMATTER_KEY |
Unrecognized frontmatter key |
INVALID_FRONTMATTER_VALUE |
Known frontmatter key with an invalid value |
FRONTMATTER_PARSE_ERROR |
Frontmatter is not valid YAML |
- Card / carousel directives — native Slack card components
- Mention resolution —
@username→<@U123>via a user-lookup callback - Attachment-color callouts — colored sidebar via legacy
attachments - 50-block auto-split — automatic message chunking past the block cap
Issues and pull requests are welcome — please include tests for new behavior. See CONTRIBUTING.md for the dev setup, commit convention, and release process.
npm install
npm test
npm run build