zudo-slack-wisdom
GitHub repository

Type to search...

to open search from anywhere

Formatting

mrkdwn vs Block Kit rich_text, escaping, mentions, links, date tokens, and when plain mrkdwn beats blocks

mrkdwn: Markdown-Adjacent, Not Markdown

Slack's mrkdwn is the formatting syntax used inside text objects (message text, section text, context elements): *bold*, _italic_, ~strikethrough~, `code`, and > for a blockquote. It looks like Markdown but isn't a Markdown dialect — there's no # heading syntax, no [text](url) link syntax, and single-asterisk *bold* (not **bold**) is bold. Treat it as its own small format, not "Slack's Markdown."

Escaping &, <, >

Slack uses &, <, and > as control characters for special parsing inside text objects — a literal < in message content that isn't opening a link/mention token will otherwise be misparsed. Convert all three to HTML entities before sending user-supplied or otherwise unpredictable text through a text object:

CharacterEntity
&&amp;
<&lt;
>&gt;

This applies to mrkdwn text objects specifically — content is not automatically escaped by the API, so a bot that interpolates external strings (a filename, a user comment, a database value) into a message must escape it itself.

All of these use the same <token|optional display text> bracket syntax:

WhatSyntaxRenders as
User mention<@U012AB3CD>@-mention, resolved to display name client-side
Channel reference<#C0123456789>#channel-name, resolved client-side
User group mention<!subteam^SAZ94GDB8>@-mention for the group
@here<!here>Notifies active members of the channel
@channel<!channel>Notifies all members, active or not
@everyone<!everyone>Notifies every member of #general
Link<https://example.com>Auto-linked URL
Link with custom text<https://example.com|Link text>Link text as the clickable label
mailto: link<mailto:user@example.com|Email User>Email User as the clickable label

A bare URL in message text is auto-linked without brackets, but wrapping it explicitly is more reliable and is required as soon as custom link text is needed.

Date Formatting Tokens

<!date^timestamp^token_string^optional_link|fallback_text> renders a Unix timestamp client-side, in each reader's own locale and timezone — the one built-in way to avoid "is that UTC or my time?" ambiguity in a bot message. token_string composes one or more of:

TokenExample output
{date_num}2014-02-18
{date}February 18th, 2014
{date_short}Feb 18, 2014
{date_long}Tuesday, February 18th, 2014
{time}6:39 AM (or 06:39 for 24-hour-locale clients)
{time_secs}6:39:45 AM
{ago}3 minutes ago

Example: <!date^1392734382^{date} at {time}|February 18th, 2014 at 6:39 AM PST> — the piped fallback text is what renders on clients that don't support the token (some notification surfaces, for example), so keep it a reasonable static approximation of the token output rather than leaving it blank.

mrkdwn vs. Block Kit rich_text

mrkdwn text objects are the right choice for a short line of formatted prose — a Section block's body, a Context line, a plain-text notification. Block Kit rich_text blocks exist for structured content mrkdwn can't express cleanly: nested bullet/numbered lists, code blocks with their own visual container, and — relevant to the Lists section — the only accepted format for Slack List text column writes, which reject plain strings outright (see Creating Lists). Default to plain mrkdwn for anything that's just formatted text; reach for rich_text when the content has real structure (lists, mixed inline styles within one paragraph) or when the surface being written to requires it.

Layout blocks that include a text object can set that object's verbatim field to false to enable automatic parsing of mentions and links in content that wasn't hand-authored with the bracket syntax above — useful when relaying externally sourced text into a block.

Escaping in Practice

The three transformations above — truncating to fit a length budget, escaping &/</>, and wrapping a fragment in mrkdwn markers like *bold* — have to run in a specific order, or each one can corrupt what the next one produces.

  1. Truncate the raw text first, before escaping and before adding any mrkdwn wrapper. Truncating after escaping risks cutting an HTML entity in half (&am instead of &amp;), and truncating after wrapping risks eating the wrapper itself.

  2. Escape &, <, > second, on the now-final-length raw text.

  3. Wrap in mrkdwn markers last. Never truncate a string that already has a wrapper applied — cutting the closing * off a *bold* span doesn't just un-bold that word, it leaves every character after the cut inside an unclosed formatting span, breaking the rest of the line.

Never run the escaper over a URL. Escaping treats & as a control character, but many URLs legitimately contain & (as a query-string separator) — escaping it to &amp; inside a link token gives Slack a literal &amp; in the href instead of &, and the link either breaks or stops auto-linking entirely. Build the escaped/truncated prose and the raw URL as separate pieces, and combine them into the final <url|text> token only after both are finished — never pass the already-composed string back through the escaper a second time.

Treat Slack's own error codes as untrusted response data before logging them. An error field in a Slack API response can carry attacker-influenced content whenever any part of the request was built from external input, so don't interpolate it into logs unexamined. Only echo it if it matches a tight allowlist pattern — ^[a-z0-9_]+$ covers every documented Slack error code — and log a generic placeholder for anything that doesn't match.

Revision History

CreatedUpdated