Mentions and Slack tokens

Markdown that already contains Slack's raw mrkdwn tokens converts to native rich text elements: user and broadcast mentions, channel links, dates, links, and emoji. No lookup is required because the tokens carry Slack IDs directly. Slack applies target existence, workspace policy, and notification settings only when your app posts the message. For agents, provide only IDs they are allowed to use and validate broadcast tokens before posting.

Supported tokens

TokenBecomesNotes
<@U0123456789>User mention (pings)U… or W… user IDs
<#C0123456789>Channel linkC…, D…, or G… channel IDs
<!here> <!channel> <!everyone>Broadcast mentionWorkspace/channel policy can restrict delivery
<!subteam^S0123456789>User group mention
<!date^1719000000^{date_short}>Localized dateOptional ^url and |fallback parts supported
<https://example.com|label>Labeled linkLabels may contain spaces
:rocket: :party-parrot:Emoji elementAny 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 when Slack accepts them. A model-generated <!channel> can notify a channel, <!everyone> applies to a workspace's general channel, and a valid <@U…> can notify that user. Delivery still follows Slack's permissions and notification settings. slackmark converts the syntax faithfully; it cannot know which notifications you intended or validate that an ID exists. 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/usergroup elements in blocks) 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 &lt;@U0123456789&gt;. 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 userIdByHandle = new Map([["sarah", "U0123456789"]]);
const channelIdByName = new Map([["deploys", "C0123456789"]]);

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.