Skip to content

heumsi/slackmd

Repository files navigation

slackmd

Write Markdown, get Slack Block Kit.

npm version license made for 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.


Why slackmd?

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).


Install

npm install @heumsi/slackmd

Requires Node.js ≥ 20. The package is pure ESM.


Quick start

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 stdin

Full flags are in the CLI reference.


Worked example

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" }] }
  ]
}

Mapping reference

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, strike, `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
![alt](url) image block
GFM table table block (column alignment preserved)
Raw HTML stripped (HTML_STRIPPED warning)
:::buttons :::context :::fields :::callout see Directives

Rendering engines

Set the engine via the engine option (or --engine on the CLI).

rich_text (default)

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.

markdown

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 markdown block as tied to AI-feature apps, so its availability for a plain bot token isn't guaranteed. Prefer rich_text when in doubt.


Directives

Slack-native components that Markdown has no syntax for, written with remark-directive syntax. Directives work in both engines.

buttonsactions block

:::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.

contextcontext block

:::context
Posted by **DeployBot** · Region: us-east-1
:::

Small, muted text (with optional images) — ideal for footers, captions, and metadata.

fieldssection with two-column fields

:::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.

calloutrich_text_quote with a leading emoji

:::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 alert block is modal-only, so callouts render as rich_text_quote blocks to display in messages.


Configuration

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 16 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 document

Unknown 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_textno text is lost (HEADER_TRUNCATED / HEADER_FALLBACK_BOLD).


CLI reference

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 emoji

API reference

import { 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;
}

Warnings and limits

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

Roadmap

  • 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

Contributing

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

License

MIT © heumsi — see LICENSE. Built in the spirit of emailmd.

About

Convert Markdown to Slack Block Kit JSON — inspired by emailmd

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors