Discord Message Behavior: Routing, Replies, and Previews
Covers how Discord messages are routed, formatted, acknowledged, and previewed in both directions. Useful for operators and developers configuring Discord channels and sessions.
Read this when
- Understanding how Discord messages flow through the gateway
- Tuning reply threading, acknowledgements, or outbound mentions
Routing, formatting, acknowledgement, and preview behaviour for Discord messages, both inbound and outbound.
Runtime model
- The Discord connection is owned by the Gateway.
- Responses to Discord messages are delivered back to Discord.
- Markdown formatting is shared by bot replies and thread-bound persona replies, covering CommonMark bold and the configured table conversion.
- After formatting and mention expansion, both delivery paths apply the caller-supplied character limits again.
- Snapshots of forwarded messages arrive at the agent along with any caption that accompanies them. Forwarded text is not classified as a typed command; only the sender's own message text is used for command classification.
- Discord guild/channel metadata is appended to the model prompt as untrusted context, not as a reply prefix visible to users. Should a model copy that envelope back, OpenClaw removes the copied metadata from outbound replies and from future replay context.
- Direct chats share the agent main session (
agent:main:main) by default (session.dmScope=main). - Guild channels get isolated session keys (
agent:<agentId>:discord:channel:<channelId>). - Group DMs are ignored unless configured otherwise (
channels.discord.dm.groupEnabled=false). - Native slash commands execute in isolated command sessions (
agent:<agentId>:discord:slash:<userId>), yet they still carryCommandTargetSessionKeyto the routed conversation session. - For text-only cron/heartbeat announce delivery to Discord, only the final assistant-visible answer is sent, once. When the agent emits multiple deliverable payloads, media and structured component payloads stay multi-message.
- A send response that lacks a Discord message ID is treated as unconfirmed. Rather than reporting success or immediately resending a duplicate, queued delivery stores the missing identity for recovery; check delivery warnings with
openclaw health --verbose.
While an earlier message waits its turn, later messages in the same channel keep arriving at the reply queue. Durable ingress admission applies to Discord messages, so collect retains queued messages as separate followup turns, the same way followup does. This stops one message from committing before another rejects a combined turn. A deferred message keeps its durable recovery state.
Message behavior
Introductions when joining a server
Once the bot joins an allowed Discord server, OpenClaw posts a single introduction for that room. The server's system channel is preferred when the bot can view and send messages in it; if not, the first text channel holding both View Channel and Send Messages permissions is used. No introduction is posted when no eligible channel can be found.
The channel name and topic feed the introduction, along with recent messages where they are available. Reading earlier messages additionally needs Read Message History; if that permission is absent, OpenClaw introduces itself from channel metadata rather than failing.
Introductions are on by default, run only for servers joined after the fact, and never fire in direct messages. To turn them off, set channels.discord.joinIntro: false, or override a single account with channels.discord.accounts.<accountId>.joinIntro. History limits, target-channel selection, once-per-room behaviour, and untrusted-content handling are covered in group join introductions.
Reply tags and native replies
Agent output can carry reply tags on Discord:
[[reply_to_current]][[reply_to:<id>]]
These are governed by channels.discord.replyToMode:
off(default): implicit reply threading is off; explicit[[reply_to_*]]tags are still respectedfirst: the implicit native reply reference is attached to the first outbound Discord message of the turnall: it is attached to every outbound messagebatched: it is attached only when the inbound event was a debounced batch of several messages, which suits setups that want native replies mainly for ambiguous bursty chats rather than every single-message turn
Context and history expose message IDs, so agents can target particular messages. When context visibility permits, replies to bot messages, automation alerts included, keep the referenced text as untrusted context. Inbound events from the bot itself are still ignored, and referenced media authored by the bot is not downloaded a second time. Inside bound threads, native replies use the bot identity and need the bot to hold permission to send there. Ordinary bound-thread messages that lack a native reply keep the webhook persona. For chunked native replies, each chunk keeps the reply target chosen for it.
Link previews
By default Discord generates rich embeds for URLs. OpenClaw suppresses those generated embeds on outbound Discord messages unless you opt in, so URLs sent by the agent remain plain links:
{
channels: {
discord: {
suppressEmbeds: false,
},
},
}
Use channels.discord.accounts.<id>.suppressEmbeds to override a single account. A single message can also pass suppressEmbeds: false through agent message-tool sends. The default link-preview setting does not suppress explicit Discord embeds payloads.
Live stream preview
Draft replies can be streamed by OpenClaw, which sends a temporary message and edits it as text arrives. Discord preview streaming defaults to progress, keeping one editable work message with no tool rows. To preview answer text, set channels.discord.streaming.mode to partial or block; to turn previews off, use off. streamMode is a legacy alias; run openclaw doctor --fix to rewrite persisted config into the current nested streaming format.
{
channels: {
discord: {
streaming: {
mode: "progress",
progress: {
maxLines: 8,
maxLineChars: 120,
toolProgress: false,
commentary: false,
},
},
},
},
}
-
offturns off edits to Discord previews. -
partialupdates one preview message as tokens come in. -
blocksends chunks sized for drafts; adjust size and breakpoints viastreaming.preview.chunk(minChars,maxChars,breakPreference), capped attextChunkLimit. A preview mode set explicitly to something other thanofftakes precedence over an inheritedagents.defaults.blockStreamingDefault: "on"; an explicitstreaming.block.enabled: truetakes precedence over the preview. When a turn cannot use previews, the inherited block delivery still applies. -
progressmaintains a single editable status draft until final delivery. It stays quiet by default: the agent's latest preamble or narration appears as a status headline, along with ๐ฌ commentary and ๐ง reasoning when they stream, โ / โธ / โข plan steps, and any approval request. Ordinary tool calls and delegated-task lifecycle events add no rows unlessstreaming.progress.toolProgressis enabled. -
Finals that carry media, errors, or an explicit reply cancel any pending preview edits.
-
streaming.progress.toolProgress: trueappends the rolling tool log below the headline: rows such as๐ ๏ธ Exec: runningor๐ Read from notes.txt(defaultfalse). Every tool row begins with a text glyph identifying its tool.streaming.preview.toolProgressgoverns tool rows inpartialandblockmodes, where they default totrue. -
streaming.progress.commentary(defaultfalse) opts into raw assistant commentary inside the temporary progress draft. The default preamble/narration status line works independently of this option. Commentary is cleaned before it is shown, remains transient, and leaves final answer delivery untouched. -
streaming.progress.maxLineCharssets the progress preview budget per line. Prose gets shortened at word boundaries; command and path details retain their useful suffixes. -
streaming.preview.commandText/streaming.progress.commandTextsets command/exec detail in compact progress lines:status(default, tool label only) orraw(explicit command text).Display the rolling tool log while suppressing raw command/exec text:
{ "channels": { "discord": { "streaming": { "mode": "progress", "progress": { "toolProgress": true, "commandText": "status" } } } } }Preview streaming handles text only; media replies revert to normal delivery.
Ack reactions
Status reactions keep the acknowledgement stable for the whole run. They add no per-tool emoji, no inactivity warnings, and no success flash. Genuine failures still follow the error reaction lifecycle.
ackReaction sends an acknowledgement emoji while OpenClaw processes an inbound message.
Resolution order:
channels.discord.accounts.<accountId>.ackReactionchannels.discord.ackReactionmessages.ackReaction- agent identity emoji fallback (
agents.entries.*.identity.emoji, else "๐")
Notes:
- Discord accepts unicode emoji or custom emoji names.
- To turn the reaction off for a channel or account, use
"".
Scope (messages.ackReactionScope):
Values: "all" (DMs + groups, including ambient room events), "direct" (DMs only), "group-all" (every group message except ambient room events, no DMs), "group-mentions" (groups when the bot is mentioned; no DMs, default), "off" / "none" (disabled).
Note
The default scope (
"group-mentions") fires no ack reactions in direct messages or ambient room events. For an ack reaction on inbound Discord DMs and quiet room events, setmessages.ackReactionScopeto"all".
Outbound mention aliases
Reach for mentionAliases when agents must mention known Discord users by handle. Keys are handles with the leading @ omitted; values are Discord user IDs. Unknown handles, @everyone, @here, and mentions found inside URLs, email addresses, or Markdown backtick code spans and fences are kept as-is.
Cached names that are ambiguous and discriminator tags that match nothing remain plain text rather than pinging a guessed user. Pick the intended person with an explicit <@userId> mention or a configured mentionAliases entry.
Known mentions keep any punctuation that follows them (., ,, !, ?, :, ;, )), and that includes dotted handles like @alice.dev.. When a handle is known, matching the longest one wins out over reading its trailing periods as punctuation.
{
channels: {
discord: {
mentionAliases: {
SupportLead: "123456789012345678",
},
accounts: {
ops: {
mentionAliases: {
OpsLead: "234567890123456789",
},
},
},
},
},
}