Mentions and Slack tokens
Markdown that already contains Slack's raw mrkdwn tokens converts to native rich text elements: mentions that actually notify people, channel links that navigate, emoji that render. No configuration or lookup is required, because the tokens carry Slack IDs directly. That makes them the natural fit for agents. Give your model the IDs from the incoming Slack event and let it write the tokens inline.
Supported tokens
| Token | Becomes | Notes |
|---|---|---|
<@U0123456789> | User mention (pings) | U… or W… user IDs |
<#C0123456789> | Channel link | C…, D…, or G… channel IDs |
<!here> <!channel> <!everyone> | Broadcast mention | |
<!subteam^S0123456789> | User group mention | |
<!date^1719000000^{date_short}> | Localized date | Optional ^url and |fallback parts supported |
<https://example.com|label> | Labeled link | Labels may contain spaces |
:rocket: :party-parrot: | Emoji element | Any well-formed name, including custom workspace emoji |
<!here> Deploy is live :rocket:
<@U0123456789> owns the rollback.
Track it in <#C0123456789>
or the <https://example.com/runbook|runbook page>.
Escalation goes to <!subteam^S0123456789>.
Freeze starts <!date^1784764800^{date_short}|Jul 23, 2026>.Block Kit JSON
…
Emoji shortcodes work because Slack accepts arbitrary emoji names and renders a
literal :name: fallback for unknown ones. That is also what makes custom
workspace emoji work without a lookup table. Times and ratios like 12:30:45
are not misparsed; a shortcode needs word boundaries on both sides.
Mentions notify real people
These tokens are live. A model-generated <!channel> pings everyone in the
channel, <!everyone> the whole workspace, and any <@U…> it invents or
copies pings that user. slackmark converts them faithfully; it cannot know
which pings you intended. Treat mention tokens as a capability you grant rather
than a formatting detail:
- Allowlist what the model may emit. Put only the user/channel IDs it is
authorized to mention into its context, and decide which broadcast tokens
(if any) are acceptable. Strip or reject disallowed tokens from the model's
output before converting. A code span keeps a token visible without pinging
(
`<!channel>`). - Check before posting. Mentions ping when the message is posted, not
when it is converted, so a scan of the model output (or the emitted
user/broadcast/usergroupelements inblocks) is a cheap final gate. - Test in a private channel. Converted output is safe to inspect anywhere, since the playground and previews never notify. Still, post test messages to a private channel with only your bot and you in it.
Escaped tokens still convert
LLMs and upstream sanitizers often HTML-escape angle brackets, producing
<@U0123456789>. These convert exactly like the raw form: the markdown
parser decodes character references before slackmark sees the text, so both
spellings produce a real mention.
To keep a token literal (for example, docs about mentions), wrap it in a code span. Inline code is never scanned for tokens:
Write `<@U0123456789>` to mention a user.Notification text
Mentions ping from blocks. The text field returned by convert() is the
plain-text notification fallback. Send it along so push notifications and
screen readers get readable content. It needs no extra mention handling.
Resolving plain @handles (optional)
By default, a bare @sarah or #deploys stays literal text; slackmark never
guesses IDs. If your app has a directory, supply resolvers and matching handles
become real mentions. Anything a resolver declines (returns undefined) stays
text:
import { createConverter } from "slackmark";
const converter = createConverter({
mentionResolvers: {
resolveUser: (handle) => userIdByHandle.get(handle), // "sarah" → "U0123456789"
resolveChannel: (name) => channelIdByName.get(name), // "deploys" → "C0123456789"
},
});Handles may contain dots (@john.doe), and sentence punctuation is not
swallowed: ping @sarah. resolves sarah, not sarah.. Email addresses like
user@example.com are never treated as mentions.
For agents, prefer raw tokens over resolvers: they are deterministic, need no directory, and survive verbatim in the model's output.