Discord Bot Setup and Configuration for OpenClaw
Learn how to set up OpenClaw on Discord, including bot creation, config keys, voice support, and troubleshooting. Ideal for server admins and developers integrating Discord.
Read this when
- Working on Discord channel features
OpenClaw operates on Discord through the official gateway as a bot. Both direct messages and guild channels are supported.
-
Pairing, Direct messages on Discord default to pairing mode.
-
Slash commands, Built-in command behavior and the command reference.
-
Channel troubleshooting, Repair and diagnostic workflows across channels.
Quick setup
Set up a Discord application with a bot, invite it to your server, and link it with OpenClaw. A private server is preferable; create one (Create My Own > For me and my friends) if you don't have one.
Create a Discord application and bot
Within the Discord Developer Portal, select New Application and give it a name (such as "OpenClaw").
Choose Bot from the sidebar and assign the Username field your agent's name.
Enable privileged intents
On the Bot page, under Privileged Gateway Intents, turn on:
- Message Content Intent (needed for standard guild messages)
- Server Members Intent (suggested; necessary for role allowlists, name-to-ID resolution, and channel-audience access groups)
- Presence Intent (optional; applies only to presence updates)
Copy your bot token
From the Bot page, click Reset Token and copy the value shown.
Note
This action actually creates your initial token, despite the label; nothing is being reset.
Generate an invite URL and add the bot to your server
Select OAuth2 in the sidebar. Within the OAuth2 URL Generator, activate these scopes:
botapplications.commands
In the Bot Permissions panel that appears, enable at minimum:
General Permissions
- View Channels
Text Permissions
- Send Messages
- Read Message History
- Embed Links
- Attach Files
- Add Reactions (optional)
This covers standard text channels. For thread posting, including forum or media channel flows that start or continue a thread, also enable Send Messages in Threads.
Copy the generated URL, load it in a browser, choose your server, and press Continue. The bot should show up in your server afterward.
Enable Developer Mode and collect your IDs
Turn on Developer Mode within the Discord client so you can copy IDs:
- User Settings (gear icon) → Developer → enable Developer Mode (on mobile: App Settings → Advanced)
- Right-click your server icon → Copy Server ID
- Right-click your own avatar → Copy User ID
Hold onto the Server ID and User ID alongside your bot token; all three are needed next.
Allow DMs from server members
For pairing to succeed, Discord must allow the bot to DM you. Right-click your server icon → Privacy Settings → enable Direct Messages.
Leave this enabled if you use Discord DMs with OpenClaw. For guild-channel-only usage, you can turn it off after pairing.
Set your bot token securely (do not send it in chat)
Treat the bot token as sensitive. Configure it on the machine hosting OpenClaw before messaging your agent:
export DISCORD_BOT_TOKEN="YOUR_BOT_TOKEN"
cat > discord.patch.json5 <<'JSON5'
{
channels: {
discord: {
enabled: true,
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
},
},
}
JSON5
openclaw config patch --file ./discord.patch.json5 --dry-run
openclaw config patch --file ./discord.patch.json5
openclaw gateway
If OpenClaw is already running as a background service, restart it through the OpenClaw Mac app or by stopping and relaunching the openclaw gateway run process.
For managed service deployments, execute openclaw gateway install from a shell where DISCORD_BOT_TOKEN is defined, or place the variable in ~/.openclaw/.env so the service can locate the env SecretRef after restart.
When Discord blocks or rate-limits your host during startup application lookup, provide the application/client ID from the Developer Portal so startup skips that REST call: channels.discord.applicationId for the default account, or channels.discord.accounts.<accountId>.applicationId per bot.
Configure OpenClaw and pair
Ask your agent
Message your OpenClaw agent on a current channel (like Telegram) and request it. If Discord is your only channel, use the CLI or config tab instead.
"I already set my Discord bot token in config. Please finish Discord setup with User ID
<user_id>and Server ID<server_id>."
CLI / config
Configuration via file:
{
channels: {
discord: {
enabled: true,
token: {
source: "env",
provider: "default",
id: "DISCORD_BOT_TOKEN",
},
},
},
}
Environment fallback for the default account:
DISCORD_BOT_TOKEN=...
For scripted or remote setup, write the same JSON5 block with openclaw config patch --file ./discord.patch.json5 --dry-run, then rerun without --dry-run. Plaintext token strings are also accepted, and SecretRef values work for channels.discord.token across env/file/exec/store providers. Refer to Secrets Management.
For multiple Discord bots, place each bot token and application ID under its own account. A top-level channels.discord.applicationId is inherited by accounts, so set it there only when every account shares the same application ID.
{
channels: {
discord: {
enabled: true,
accounts: {
personal: {
token: { source: "env", provider: "default", id: "DISCORD_PERSONAL_TOKEN" },
applicationId: "111111111111111111",
},
work: {
token: { source: "env", provider: "default", id: "DISCORD_WORK_TOKEN" },
applicationId: "222222222222222222",
},
},
},
},
}
Approve first DM pairing
Once the gateway is live, DM your bot in Discord. A pairing code comes back in reply.
Ask your agent
Forward the pairing code to your agent on your existing channel:
"Approve this Discord pairing code:
<CODE>"
CLI
openclaw pairing list discord
openclaw pairing approve discord <CODE>
Pairing codes remain valid for 1 hour. After approval, converse with your agent in a Discord DM.
If Message Content Intent is unavailable from Discord, OpenClaw still functions in DMs and in
guild channels where users explicitly @mention the bot. Set
channels.discord.intents.messageContent: false so the Gateway avoids requesting the
unavailable privileged intent, and keep requireMention: true on every configured guild
channel. In this mode, Discord withholds user-authored content from other guild messages.
Note
Token resolution depends on the account. Config token values take precedence over the env fallback, and
DISCORD_BOT_TOKENapplies only to the default account. When two enabled Discord accounts resolve to the same bot token, OpenClaw starts just one gateway monitor for that token: a config-sourced token beats the env fallback; otherwise the first enabled account wins and the duplicate account is reported disabled with reasonduplicate bot token. For advanced outbound calls (message tool/channel actions), an explicit per-calltokenis used for that call. This covers send and read/probe-style actions (read/search/fetch/thread/pins/permissions). Account policy/retry settings still come from the selected account in the active runtime snapshot.
Recommended: Set up a guild workspace
Once DMs are working, you can convert your server into a full workspace where each channel hosts its own agent session with separate context. Best for private servers containing only you and your bot.
Add your server to the guild allowlist
This enables your agent to reply in any channel on your server, not just DMs.
Ask your agent
"Add my Discord Server ID
<server_id>to the guild allowlist"
Config
{
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
YOUR_SERVER_ID: {
requireMention: true,
users: ["YOUR_USER_ID"],
},
},
},
},
}
Allow responses without @mention
By default, the agent replies in guild channels only when @mentioned. On a private server you likely want responses to every message.
In guild channels, normal replies post automatically by default. For shared always-on rooms, opt into messages.groupChat.visibleReplies: "message_tool" so the agent can lurk and only post when it judges a channel reply useful. This performs best with latest-generation, tool-reliable models such as GPT-5.6 Sol. Ambient room events stay quiet unless the tool sends. See Ambient room events for the complete lurk-mode configuration.
If Discord shows typing and the logs indicate token usage but no message posts, verify whether the turn was set as an ambient room event or opted into message-tool visible replies.
Ask your agent
"Allow my agent to respond on this server without having to be @mentioned"
Config
Set requireMention: false in your guild config:
{
channels: {
discord: {
guilds: {
YOUR_SERVER_ID: {
requireMention: false,
},
},
},
},
}
To force message-tool sends for visible group/channel replies, set messages.groupChat.visibleReplies: "message_tool".
Plan for memory in guild channels
Long-term memory (MEMORY.md) auto-loads only in DM sessions; guild channels do not load it.
Ask your agent
"When I ask questions in Discord channels, use memory_search or memory_get if you need long-term context from MEMORY.md."
Manual
For shared context across every channel, put stable instructions in AGENTS.md or USER.md (injected for each session). Keep long-term notes in MEMORY.md and retrieve them on demand with memory tools.
Now create channels and begin chatting. The agent sees the channel name, and each channel is an isolated session, so set up #coding, #home, #research, or whatever fits your workflow.
Runtime model
- The Discord connection is managed by the gateway.
- Reply routing follows a fixed pattern: any inbound reply on Discord is sent back to Discord.
- Metadata about the Discord guild and channel gets injected into the model prompt as untrusted context, not as a visible prefix on user replies. Should the model echo that envelope, OpenClaw removes the duplicated metadata from outbound messages and from any future replay context.
- By default (
session.dmScope=main), direct messages share the agent's primary session (agent:main:main). - Guild channels use isolated session keys (
agent:<agentId>:discord:channel:<channelId>). - Group DMs are skipped unless explicitly enabled (
channels.discord.dm.groupEnabled=false). - Native slash commands run in their own command sessions (
agent:<agentId>:discord:slash:<userId>), while still passingCommandTargetSessionKeyto the routed conversation session. - For text-only cron or heartbeat announcements delivered to Discord, the output condenses to the final assistant-visible answer and is sent once. Media and structured component payloads, however, are still sent as multiple messages when the agent emits several deliverable payloads.
Forum channels
Thread posts are the only accepted format in Discord forum and media channels. OpenClaw offers two ways to create them:
- Post a message to the forum parent (
channel:<forumId>) to automatically generate a thread. The thread's title comes from the first non-empty line of the message, truncated to Discord's 100-character thread-name limit. - Use
openclaw message thread createto directly create a thread. For forum channels, avoid passing--message-id.
To create a thread by sending to the forum parent:
openclaw message send --channel discord --target channel:<forumId> \
--message "Topic title\nBody of the post"
To explicitly create a forum thread:
openclaw message thread create --channel discord --target channel:<forumId> \
--thread-name "Topic title" --message "Body of the post"
Discord components are not accepted by forum parents. If components are needed, send them to the thread itself (channel:<threadId>).
Interactive components
Discord components v2 containers are supported for agent messages. Use the message tool with a components payload. Interaction results return to the agent as standard inbound messages and follow the existing Discord replyToMode settings.
components is a Discord-specific addition to the shared message tool. OpenClaw makes it available whenever Discord is configured, even if another channel is currently active. Use presentation when the same rich message needs to work across channels; OpenClaw adjusts portable presentation actions for each target.
Supported blocks:
text,section,separator,actions,media-gallery,file- Action rows can hold up to 5 buttons or one select menu
- Select types:
string,user,role,mentionable,channel
Components are single-use by default. To let buttons, selects, and forms fire repeatedly until they expire, set components.reusable=true.
For restricting button access, assign allowedUsers to the button, using Discord user IDs, tags, or *. Anyone not matching gets an ephemeral denial.
Callback expiry defaults to 30 minutes. Adjust it with channels.discord.agentComponents.ttlMs for the default account, or channels.discord.accounts.<accountId>.agentComponents.ttlMs on a per-account basis. The value is in milliseconds, must be a positive integer, and tops out at 86400000 (24 hours). Longer TTLs fit review or approval flows where buttons need to stay active, but they widen the period during which an old Discord message can still fire an action. Pick the shortest TTL that works, and stick with the default if stale callbacks would catch users off guard.
The /model and /models slash commands launch an interactive model picker with provider, model, and compatible runtime dropdowns, plus a Submit step. /models add is deprecated and responds with a deprecation notice rather than registering models from chat. The picker reply is ephemeral and available only to the user who invoked it. Since Discord select menus cap at 25 options, add provider/* entries to agents.defaults.modelPolicy.allow if you want the picker to display dynamically discovered models solely for specific providers like openai or vllm.
File attachments:
fileblocks must reference an attachment (attachment://<filename>)- Supply the attachment via
media/path/filePath(single file); usemedia-galleryfor multiple files - Override the upload name with
filenamewhen it should align with the attachment reference
Modal forms:
- Add
components.modalwith up to 5 fields - Field types:
text,checkbox,radio,select,role-select,user-select - OpenClaw adds a trigger button automatically
Example:
{
channel: "discord",
action: "send",
to: "channel:123456789012345678",
message: "Optional fallback text",
components: {
reusable: true,
text: "Choose a path",
blocks: [
{
type: "actions",
buttons: [
{
label: "Approve",
style: "success",
allowedUsers: ["123456789012345678"],
},
{ label: "Decline", style: "danger" },
],
},
{
type: "actions",
select: {
type: "string",
placeholder: "Pick an option",
options: [
{ label: "Option A", value: "a" },
{ label: "Option B", value: "b" },
],
},
},
],
modal: {
title: "Details",
triggerLabel: "Open form",
fields: [
{ type: "text", label: "Requester" },
{
type: "select",
label: "Priority",
options: [
{ label: "Low", value: "low" },
{ label: "High", value: "high" },
],
},
],
},
},
}
Access control and routing
DM policy
DM access is governed by channels.discord.dmPolicy. The canonical DM allowlist is channels.discord.allowFrom.
pairing(default)allowlist(needs at least oneallowFromsender)open(requireschannels.discord.allowFromto include"*")disabled
When DM policy isn't open, unknown users get blocked, or in pairing mode they're prompted for pairing.
Precedence across multiple accounts:
- When account
dmPolicyandgroupPolicyare omitted, they take on the channel root. Explicit account policies override; if neither scope is set, the defaults remainpairingandallowlistin that order. - Only the
defaultaccount is affected bychannels.discord.accounts.default.allowFrom. - For a single account,
allowFromoutranks the olderdm.allowFrom. - Named accounts fall back to
channels.discord.allowFromwhen their ownallowFromand legacydm.allowFromaren't set. - Named accounts don't inherit
channels.discord.accounts.default.allowFrom.
For compatibility, legacy channels.discord.dm.policy and channels.discord.dm.allowFrom are still processed. When it can do so without altering access, openclaw doctor --fix converts them to dmPolicy and allowFrom.
DM target format for delivery:
user:<id><@id>mention
Bare numeric IDs typically resolve as channel IDs when a channel default is active, but IDs in the account's effective DM allowFrom list are handled as user DM targets for backward compatibility.
Access groups
Dynamic accessGroup:<name> entries in channels.discord.allowFrom can back Discord DMs and text command authorization.
Access group names are shared across message channels. For a static group whose members use each channel's standard allowFrom syntax, go with type: "message.senders"; alternatively, type: "discord.channelAudience" makes membership dynamic based on a Discord channel's current ViewChannel audience. See Access groups for shared behavior.
{
accessGroups: {
operators: {
type: "message.senders",
members: {
"*": ["global-owner-id"],
discord: ["discord:123456789012345678"],
telegram: ["987654321"],
},
},
},
channels: {
discord: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:operators"],
},
},
}
A Discord text channel lacks its own member list. type: "discord.channelAudience" defines membership this way: the DM sender belongs to the configured guild and currently holds effective ViewChannel permission on the configured channel once role and channel overwrites are applied.
Example: let anyone who can view #maintainers DM the bot, while everyone else stays blocked from DMs.
{
accessGroups: {
maintainers: {
type: "discord.channelAudience",
guildId: "1456350064065904867",
channelId: "1456744319972282449",
membership: "canViewChannel",
},
},
channels: {
discord: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:maintainers"],
},
},
}
Static and dynamic entries can be combined:
{
accessGroups: {
maintainers: {
type: "discord.channelAudience",
guildId: "1456350064065904867",
channelId: "1456744319972282449",
},
},
channels: {
discord: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:maintainers", "discord:123456789012345678"],
},
},
}
Lookups fail closed. If Discord returns Missing Access, the member lookup fails, or the channel belongs to another guild, the DM sender is deemed unauthorized.
When using channel-audience access groups, enable the Discord Developer Portal Server Members Intent. DMs don't carry guild member state, so OpenClaw fetches the member via Discord REST at authorization time.
Guild policy
channels.discord.groupPolicy governs guild handling:
-
open -
allowlist -
disabledWhen
channels.discordis present, the secure baseline isallowlist.allowlistbehavior: -
guild must match
channels.discord.guilds(idpreferred, slug accepted) -
optional sender allowlists:
users(stable IDs recommended) androles(role IDs only); if either is set, senders are allowed when they matchusersORroles -
direct name/tag matching is off by default; turn on
channels.discord.dangerouslyAllowNameMatching: trueonly as break-glass compatibility mode -
names/tags work for
users, but IDs are safer;openclaw security auditwarns when name/tag entries appear -
if a guild has
channelsconfigured, channels not listed are denied -
if a guild lacks a
channelsblock, every channel in that allowlisted guild is permitted
Example:
{
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
"123456789012345678": {
requireMention: true,
ignoreOtherMentions: true,
users: ["987654321098765432"],
roles: ["123456789012345678"],
channels: {
general: { enabled: true },
help: { enabled: true, requireMention: true },
},
},
},
},
},
}
The openclaw doctor --fix migration converts the older per-channel allow key into enabled.
If a channels.discord block is absent, the Gateway won't start Discord automatically from DISCORD_BOT_TOKEN. After that block is present, DISCORD_BOT_TOKEN continues to serve as the fallback for the default account token. Choosing --ambient-channels enables environment-only auto-configuration, which relies on groupPolicy="allowlist" and emits a warning, even when channels.defaults.groupPolicy is set to open.
Mentions and group DMs
Guild messages are gated on mentions by default.
Mention detection covers:
- direct bot mentions
- configured mention patterns (
agents.entries.*.groupChat.mentionPatterns, withmessages.groupChat.mentionPatternsas fallback) - implicit reply-to-bot handling where supported
For outbound Discord messages, stick to canonical mention syntax: <@USER_ID> targets users, <#CHANNEL_ID> targets channels, and <@&ROLE_ID> targets roles. Avoid the older <@!USER_ID> nickname mention format.
Per guild or channel, requireMention is set via channels.discord.guilds....
Optionally, ignoreOtherMentions filters out messages that mention another user or role but not the bot, leaving @everyone and @here untouched.
Group DMs behave as follows:
- ignored by default (
dm.groupEnabled=false) - an optional allowlist via
dm.groupChannels(channel IDs or slugs) can include them
Guild channel maps are allowlists
When a guild entry lacks an channels map, the bot operates in every visible channel, limited only by the guild's requireMention and users rules. Introducing a single channel entry flips the map into an allowlist: any channel without a matching entry is blocked outright, rather than falling back to guild defaults.
This often catches people off guard: they add one channel for custom settings and suddenly the bot goes quiet elsewhere. Keep the rest of the guild accessible by using the "*" wildcard key:
{
channels: {
discord: {
guilds: {
YOUR_SERVER_ID: {
requireMention: true,
users: ["YOUR_USER_ID"],
channels: {
// always-on room: everyone in it can talk to the bot, no mention needed
YOUR_CHANNEL_ID: { enabled: true, requireMention: false, users: ["*"] },
// every other channel keeps the guild defaults
"*": { enabled: true, requireMention: true },
},
},
},
},
},
}
Channel entries take precedence over guild-level settings, so a channel entry with users: ["*"] lets any sender into that one room even when the guild's users list is restrictive. Entries match by channel ID, name, or slug, and a thread inherits its parent channel's entry.
Role-based agent routing
To assign Discord guild members to different agents based on role, use bindings[].match.roles. Role-based bindings only accept role IDs and are checked after peer or parent-peer bindings but before guild-only ones. If a binding also specifies other match fields (such as peer plus guildId plus roles), every configured field must match.
{
bindings: [
{
agentId: "opus",
match: {
channel: "discord",
guildId: "123456789012345678",
roles: ["111111111111111111"],
},
},
{
agentId: "sonnet",
match: {
channel: "discord",
guildId: "123456789012345678",
},
},
],
}
Native commands and command auth
commands.nativehas a default of"auto"and is turned on for Discord.- To override per channel, use
channels.discord.commands.native. commands.native=falsebypasses Discord slash-command registration and cleanup at startup. Commands registered earlier may stay visible in Discord until you manually remove them from the Discord app.- Native command auth relies on the same Discord allowlists and policies as regular message handling.
- Unauthorized users may still see commands in the Discord UI; execution enforces OpenClaw auth and responds with "not authorized".
- Slash command defaults:
ephemeral: true(channels.discord.slashCommand.ephemeral).
For the full command catalog and behavior, see Slash commands.
Feature details
Introductions when joining a server
Upon joining an allowed Discord server, OpenClaw sends a single room-specific introduction. It picks the server's system channel if the bot can view and post there; otherwise it falls back to the first text channel granting both View Channel and Send Messages permissions. If no channel qualifies, no introduction goes out.
Introductions draw on the channel name and topic, plus recent messages when they exist. Reading earlier messages additionally demands Read Message History; lacking that permission, OpenClaw still introduces itself using channel metadata rather than erroring out.
These introductions are on by default, fire only for newly joined servers, and never occur in direct messages. Turn them off with channels.discord.joinIntro: false, or override a single account with channels.discord.accounts.<accountId>.joinIntro. Check group join introductions for history limits, target-channel selection, once-per-room behavior, and untrusted-content handling.
Reply tags and native replies
Agent output on Discord can include reply tags:
[[reply_to_current]][[reply_to:<id>]]
channels.discord.replyToMode governs this:
off(default): no implicit reply threading, though explicit[[reply_to_*]]tags still workfirst: adds the implicit native reply reference to the first outbound Discord message of the turnall: adds it to every outbound messagebatched: adds it only when the inbound event was a debounced batch of multiple messages, which suits cases where native replies matter mainly for ambiguous bursty chats rather than every single-message turn
Message IDs appear in context and history so agents can aim at specific messages.
Link previews
By default, Discord turns URLs into rich link embeds. OpenClaw suppresses those generated embeds on outbound Discord messages by default, so agent-sent URLs appear as plain links unless you opt in:
{
channels: {
discord: {
suppressEmbeds: false,
},
},
}
Set channels.discord.accounts.<id>.suppressEmbeds to override a single account. Agent message-tool sends may also pass suppressEmbeds: false for one message. Explicit Discord embeds payloads are not suppressed by the default link-preview setting.
Live stream preview
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. Discord preview streaming defaults to off; set channels.discord.streaming.mode to partial, block, or progress to opt in. streamMode is a legacy alias; run openclaw doctor --fix to rewrite persisted config to the canonical nested streaming shape.
{
channels: {
discord: {
streaming: {
mode: "progress",
progress: {
maxLines: 8,
maxLineChars: 120,
toolProgress: false,
commentary: false,
},
},
},
},
}
offdisables Discord preview edits.partialedits a single preview message as tokens arrive.blockemits draft-sized chunks; tune size and breakpoints withstreaming.preview.chunk(minChars,maxChars,breakPreference), clamped totextChunkLimit. An explicit non-offpreview mode overrides inheritedagents.defaults.blockStreamingDefault: "on"; explicitstreaming.block.enabled: trueoverrides the preview. If a turn cannot use previews, inherited block delivery still applies.progresskeeps one editable status draft until final delivery. It shows the agent's latest preamble or narration as a status headline, with the compact tool rows underneath and no generated label.- Media, error, and explicit-reply finals cancel pending preview edits.
streaming.preview.toolProgressandstreaming.progress.toolProgressboth default totruewhen preview streaming is active. Tool rows such as🛠️ Bash: run testsor🔎 Web Search: for "query"need no additional progress config; set either key tofalseto keep the status headline only.streaming.progress.commentary(defaultfalse) opts into raw assistant commentary in the temporary progress draft. The default preamble/narration status line is independent of this option. Commentary is cleaned before display, stays transient, and does not change final answer delivery.streaming.progress.maxLineCharscontrols the per-line progress preview budget. Prose is shortened on word boundaries; command and path details keep useful suffixes.streaming.preview.commandText/streaming.progress.commandTextcontrols command/exec detail in compact progress lines:status(default, tool label only) orraw(explicit command text).
Hide raw command/exec text while keeping compact progress lines:
{
"channels": {
"discord": {
"streaming": {
"mode": "progress",
"progress": {
"toolProgress": true,
"commandText": "status"
}
}
}
}
}
Preview streaming is text-only; media replies fall back to normal delivery.
History, context, and thread behavior
Guild history context:
channels.discord.historyLimitdefault20- fallback:
messages.groupChat.historyLimit 0disables
DM history controls:
channels.discord.dmHistoryLimitchannels.discord.dms["<user_id>"].historyLimit
Thread behavior:
- Discord threads route as channel sessions and inherit parent channel config unless overridden.
- Thread sessions inherit the parent channel's session-level
/modelselection as a model-only fallback; thread-local/modelselections take precedence, and parent transcript history is not copied unless transcript inheritance is enabled. channels.discord.thread.inheritParent(defaultfalse) opts new auto-threads into seeding from the parent transcript. Per-account override:channels.discord.accounts.<id>.thread.inheritParent.- Message-tool reactions can resolve
user:<id>DM targets. guilds.<guild>.channels.<channel>.requireMention: falseis preserved during reply-stage activation fallback.
Channel topics are injected as untrusted context. Allowlists gate who can trigger the agent, not a full supplemental-context redaction boundary.
Thread-bound sessions for subagents
Discord can bind a thread to a session target so follow-up messages in that thread keep routing to the same session (including subagent sessions).
Commands:
/focus <target>bind current/new thread to a subagent/session target/unfocusremove current thread binding/agentsshow active runs and binding state/session idle <duration|off>inspect/update inactivity auto-unfocus for focused bindings/session max-age <duration|off>inspect/update hard max age for focused bindings
Config:
{
session: {
threadBindings: {
enabled: true,
idleHours: 24,
maxAgeHours: 0,
spawnSessions: true,
defaultSpawnContext: "fork",
},
},
}
Notes:
-
For Discord and Telegram,
session.threadBindings.*serves as the standard policy. -
Thread auto-create/bind behavior for
sessions_spawn({ thread: true })and ACP thread spawns is governed byspawnSessions, which defaults totrue. -
Native subagent context for thread-bound spawns is managed by
defaultSpawnContext, with"fork"as its default. -
The deprecated
spawnSubagentSessionsandspawnAcpSessionskeys undergo migration viaopenclaw doctor --fix. -
When thread bindings are turned off,
/focusand its associated operations become unavailable.Refer to Sub-agents, ACP Agents, and Configuration Reference.
Persistent ACP channel bindings
For persistent "always-on" ACP workspaces, set up top-level typed ACP bindings that point at Discord conversations.
Configuration path: bindings[], using type: "acp" and match.channel: "discord".
{
agents: {
entries: {
codex: {
runtime: {
type: "acp",
acp: {
agent: "codex",
backend: "acpx",
mode: "persistent",
cwd: "/workspace/openclaw",
},
},
},
},
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "discord",
accountId: "default",
peer: { kind: "channel", id: "222222222222222222" },
},
acp: { label: "codex-main" },
},
],
channels: {
discord: {
guilds: {
"111111111111111111": {
channels: {
"222222222222222222": {
requireMention: false,
},
},
},
},
},
},
}
Notes:
-
The current channel or thread gets bound in place by
/acp spawn codex --bind here, ensuring subsequent messages stay on the same ACP session. Bindings from thread messages carry over from the parent channel. -
Within a bound channel or thread,
/newand/resetreset that ACP session in place. While active, temporary thread bindings can override how targets are resolved. -
Child thread creation and binding is gated by
spawnSessionsthrough--thread auto|here.Binding behavior specifics are covered in ACP Agents.
Reaction notifications
Per-guild reaction notification mode (guilds.<id>.reactionNotifications):
offown(the default)allallowlist(relies onguilds.<id>.users)
Reaction events get converted into system events and are attached to the routed Discord session.
Online presence events
When a human member goes from offline to online, opt a guild into routed agent wakes:
{
channels: {
discord: {
intents: { presence: true },
guilds: {
"111111111111111111": {
presenceEvents: {
channelId: "222222222222222222",
users: ["333333333333333333"], // optional; further narrow channel viewers
reconnectSuppressSeconds: 300, // optional; new-session quiet window (0 disables)
burstLimit: 8, // optional; max events per burst window
burstWindowSeconds: 60, // optional; sliding burst-detection window
},
},
},
},
},
}
For presenceEvents to work, the routed agent must have an enabled heartbeat, and the Presence Intent privilege must be active on the application's Bot page in the Discord Developer Portal. OpenClaw seeds current online members from each complete GUILD_CREATE snapshot, routes observed offline-to-online transitions, and also treats a later first online signal for an unseen member as newly available. That member may have come online or joined after the snapshot, so the event does not assert an exact prior status. Only humans who can view channelId are eligible: channels and public threads require View Channel on the channel or parent, while private threads additionally require membership or Manage Threads. users can further narrow that audience. OpenClaw ignores bots and unchanged online states and persists an eight-hour per-user cooldown across Gateway restarts. When Discord establishes a new Gateway session and sends READY, OpenClaw suppresses presence-derived events for reconnectSuppressSeconds (default 300, 0 disables) while guild presence state is rebuilt, so re-observed members cannot wake the agent one by one. It additionally rate-limits successfully queued events per guild to burstLimit events (default 8) per burstWindowSeconds sliding window (default 60), logging each guild's suppression episode once. A resumed session is not treated as a new session. Discord limits snapshots for guilds above 75,000 members; there, OpenClaw requires an explicit offline update before greeting. The system event carries immutable user, guild, and channel IDs without embedding mutable display names. The agent decides whether and how to greet.
Ack reactions
While OpenClaw processes an inbound message, ackReaction dispatches an acknowledgement emoji.
Resolution order:
channels.discord.accounts.<accountId>.ackReactionchannels.discord.ackReactionmessages.ackReaction- agent identity emoji fallback (
agents.entries.*.identity.emoji, otherwise "👀")
Notes:
- Either unicode emoji or custom emoji names are accepted by Discord.
- Set
""to turn off the reaction for a specific channel or account.
Scope (messages.ackReactionScope):
Possible values: "all" (DMs plus groups, covering ambient room events), "direct" (DMs only), "group-all" (all group messages except ambient room events, no DMs), "group-mentions" (groups when the bot is mentioned; no DMs, the default), "off" / "none" (disabled).
Note
With the default scope (
"group-mentions"), ack reactions are not fired in direct messages or ambient room events. To receive an ack reaction for inbound Discord DMs and quiet room events, assignmessages.ackReactionScopethe value"all".
Config writes
Channel-initiated config writes are on by default, which impacts /config set|unset flows when command features are enabled.
To turn this off:
{
channels: {
discord: {
configWrites: false,
},
},
}
Gateway proxy
Route Discord gateway WebSocket traffic and startup REST lookups (application ID plus allowlist resolution) through an HTTP(S) proxy using channels.discord.proxy. Proxying for the Discord gateway WebSocket is explicit; these connections do not pick up ambient proxy environment variables from the Gateway process. Startup REST lookups will use this proxy when channels.discord.proxy is set.
{
channels: {
discord: {
proxy: "http://proxy.example:8080",
},
},
}
Override per account:
{
channels: {
discord: {
accounts: {
primary: {
proxy: "http://proxy.example:8080",
},
},
},
},
}
PluralKit support
Turn on PluralKit resolution so proxied messages map to system member identity:
{
channels: {
discord: {
pluralkit: {
enabled: true,
token: "pk_live_...", // optional; needed for private systems
},
},
},
}
Notes:
- allowlists may reference
pk:<memberId> - member display names are matched by name or slug only when
channels.discord.dangerouslyAllowNameMatching: trueis active - lookups hit the PluralKit API with the original message ID
- if a lookup fails, proxied messages are handled as bot messages and discarded unless
allowBotspermits them
Outbound mention aliases
Use mentionAliases when agents need deterministic outbound mentions for known Discord users. Keys are handles without the leading @; values are Discord user IDs. Unknown handles, @everyone, @here, and mentions inside Markdown code spans stay as they are.
{
channels: {
discord: {
mentionAliases: {
SupportLead: "123456789012345678",
},
accounts: {
ops: {
mentionAliases: {
OpsLead: "234567890123456789",
},
},
},
},
},
}
Presence configuration
Presence updates take effect when you set a status or activity field, or when auto presence is enabled.
Status only:
{
channels: {
discord: {
status: "idle",
},
},
}
Activity (custom status is the default activity type when activity is set):
{
channels: {
discord: {
activity: "Focus time",
activityType: 4,
},
},
}
Streaming:
{
channels: {
discord: {
activity: "Live coding",
activityType: 1,
activityUrl: "https://twitch.tv/openclaw",
},
},
}
Activity type mapping:
-
0: Playing
-
1: Streaming (needs
activityUrl;activityUrlin turn needsactivityType: 1) -
2: Listening
-
3: Watching
-
4: Custom (uses the activity text as the status state; emoji is optional)
-
5: Competing
Auto presence (runtime health signal):
{
channels: {
discord: {
autoPresence: {
enabled: true,
intervalMs: 30000,
minUpdateIntervalMs: 15000,
},
},
},
}
Auto presence translates runtime availability into Discord status: healthy becomes online, degraded or unknown becomes idle, exhausted or unavailable becomes dnd. Defaults: intervalMs 30000, minUpdateIntervalMs 15000 (must not exceed intervalMs).
Approvals in Discord
Discord supports button-based approval handling in DMs and can optionally post approval prompts in the originating channel.
Config path:
channels.discord.execApprovals.enabledchannels.discord.execApprovals.approvers(optional; falls back tocommands.ownerAllowFromwhen possible)channels.discord.execApprovals.target(dm|channel|both, default:dm)agentFilter,sessionFilter,cleanupAfterResolve
Discord native exec approvals require enabled: true or enabled: "auto" and at least one resolved approver, sourced from either execApprovals.approvers or commands.ownerAllowFrom. Leaving enabled unset or setting it to false disables native exec approval delivery. Discord does not infer exec approvers from channel allowFrom, legacy dm.allowFrom, or direct-message defaultTo.
For sensitive owner-only group commands such as /diagnostics and /export-trajectory, OpenClaw sends approval prompts and final results privately. It tries Discord DM first when the invoking owner has a Discord owner route; otherwise it falls back to the first available owner route from commands.ownerAllowFrom, such as Telegram.
When target is channel or both, the approval prompt is visible in the channel. Only resolved approvers can use the buttons; other users receive an ephemeral denial. Approval prompts include the command text, so only enable channel delivery in trusted channels. If the channel ID cannot be derived from the session key, OpenClaw falls back to DM delivery.
Discord renders the shared approval buttons used by other chat channels; the native Discord adapter mainly adds approver DM routing and channel fanout. When those buttons are present, they are the primary approval UX; OpenClaw should only include a manual /approve command when the tool result says chat approvals are unavailable or manual approval is the only path. If the Discord native approval runtime is not active, OpenClaw keeps the local deterministic /approve <id> <decision> prompt visible. If the runtime is active but a native card cannot be delivered to any target, OpenClaw sends a same-chat fallback notice with the exact /approve command from the pending approval.
Gateway authentication and approval resolution follow the shared Gateway client contract (plugin: IDs are resolved via plugin.approval.resolve; all other IDs go through exec.approval.resolve). By default, approvals lapse after 30 minutes.
Refer to Exec approvals.
Tools and action gates
Discord message actions handle messaging, channel administration, moderation, presence, and metadata.
Core examples:
- messaging:
sendMessage,readMessages,editMessage,deleteMessage,threadReply - reactions:
react,reactions,emoji-list - moderation:
timeout,kick,ban - presence:
setPresence
To list the custom emoji available on the current server, use emoji-list:
{ "action": "emoji-list", "channel": "discord", "limit": 25 }
guildId points to the server of the active conversation by default; specify it explicitly to query a different server. Entries come back ordered by name, and limit has a default of 100 with 100 as its hard ceiling:
{
"ok": true,
"emojis": [
{ "name": "dance", "identifier": "dance:456", "animated": true },
{ "name": "party", "identifier": "party:123" }
]
}
Hand identifier straight to react. Discord supports Unicode emoji, custom name:id IDs, and the <:name:id> or <a:name:id> formats. emoji-list, react, and reactions all fall under the control of channels.discord.actions.reactions.
The event-create action takes an optional image argument (a URL or a local file path) to assign a cover image to the scheduled event.
Action gates reside under channels.discord.actions.*.
Default gate behavior:
| Action group | Default |
|---|---|
| reactions, messages, threads, pins, polls, search, memberInfo, roleInfo, channelInfo, channels, voiceStatus, events, stickers, emojiUploads, stickerUploads, permissions | enabled |
| roles | disabled |
| moderation | disabled |
| presence | disabled |
Components v2 UI
OpenClaw relies on Discord components v2 for exec approvals and cross-context markers. Discord message actions also accept components for custom UI (advanced; you must build a component payload with the discord tool), while the older embeds are still available but not advised.
channels.discord.agentComponents.ttlMsdetermines how long sent Discord component callbacks stay registered (default1800000, max86400000). Per account:channels.discord.accounts.<id>.agentComponents.ttlMs.- When components v2 are present,
embedsare disregarded. - URL previews are turned off by default. Set
suppressEmbeds: falseon a message action to expand a single outbound link.
Voice
Discord offers two separate voice surfaces: realtime voice channels (ongoing conversations) and voice message attachments (the waveform preview format). The gateway supports both.
Voice channels
Setup checklist:
- Turn on Message Content Intent in the Discord Developer Portal.
- Turn on Server Members Intent when role or user allowlists are in use.
- Invite the bot with the
botandapplications.commandsscopes. - In the target voice channel, grant Connect, Speak, Send Messages, and Read Message History.
- Enable native commands (
commands.nativeorchannels.discord.commands.native). - Set up
channels.discord.voice.
Sessions are managed with /vc join|leave|status. This command uses the account default agent and obeys the same allowlist and group policy rules as other Discord commands.
/vc join channel:<voice-channel-id>
/vc status
/vc leave
To check the bot's effective permissions before joining:
openclaw channels capabilities --channel discord --target channel:<voice-channel-id>
Auto-join example:
{
channels: {
discord: {
voice: {
enabled: true,
model: "openai/gpt-5.6-sol",
autoJoin: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
whenOccupied: true,
},
],
allowedChannels: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
},
],
daveEncryption: true,
decryptionFailureTolerance: 24,
connectTimeoutMs: 30000,
reconnectGraceMs: 15000,
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
},
},
},
},
}
Notes:
- The OpenAI
agent-proxyresponse and wake-name policies below require a GA realtime model, such asgpt-realtime-2.1. GPT-Live currently handles audio autonomously and skips those policies; avoid depending on wake-name gating with GPT-Live in a shared voice channel. - For text-only configs, Discord voice is opt-in; enable
channels.discord.voice.enabled=true(or retain an existingchannels.discord.voiceblock) to activate/vccommands, the voice runtime, and theGuildVoiceStatesgateway intent.channels.discord.intents.voiceStatescan explicitly override the intent subscription; leave it unset to follow effective voice enablement. - The conversation path is governed by
voice.mode. The default isagent-proxy: a realtime voice front end manages turn timing, interruption, and playback, delegates substantive work to the routed OpenClaw agent viaopenclaw_agent_consult, and treats the result like a typed Discord prompt from that speaker.stt-ttspreserves the older batch STT plus TTS flow.bidilets the realtime model converse directly while exposingopenclaw_agent_consultfor the OpenClaw brain. - Which OpenClaw conversation receives voice turns is controlled by
voice.agentSession. Leave it unset for the voice channel's own session, or set{ mode: "target", target: "channel:<text-channel-id>" }to make the voice channel act as the microphone/speaker extension of an existing Discord text channel session such as#maintainers. voice.modeloverrides the OpenClaw agent brain for Discord voice responses and realtime consults. Leave it unset to inherit the routed agent model. It is distinct fromvoice.realtime.model.voice.followUsersallows the bot to join, move, and leave Discord voice with selected users. See Follow users in voice.- Speech is routed through
agent-proxyviadiscord-voice, which keeps normal owner/tool authorization for the speaker and target session but hides the agentttstool because Discord voice owns playback. By default,agent-proxygrants the consult full owner-equivalent tool access for owner speakers (voice.realtime.toolPolicy: "owner") and strongly prefers consulting the OpenClaw agent before substantive answers (voice.realtime.consultPolicy: "always"). In that defaultalwaysmode, the realtime layer does not auto-speak filler before the consult answer; it captures and transcribes speech, then speaks the routed OpenClaw answer. If multiple forced consult answers finish while Discord is still playing the first answer, later exact-speech answers are queued until playback idles instead of replacing speech mid-sentence. - Realtime voice buffers generated audio when Discord playback temporarily falls behind and tolerates brief provider or network gaps. Normal backpressure does not cancel the response, and queued answers wait until Discord finishes playing the previous answer, even if its provider response or audio encoder has already finished.
- If the realtime provider ends the session, OpenClaw leaves the voice channel and clears its connected status. Check the
realtime session failed terminallylog, then use/vc jointo reconnect. Temporary provider reconnects do not end the Discord voice session. - In
stt-ttsmode, STT usestools.media.audio;voice.modeldoes not affect transcription. stt-ttsreplies remain active until Discord finishes playing them; long responses are not cut off by a fixed one-minute playback deadline.- In realtime modes,
voice.realtime.provider,voice.realtime.model, andvoice.realtime.speakerVoiceconfigure the realtime audio session. For OpenAI Realtime 2.1 plus the Codex brain, usevoice.realtime.model: "gpt-realtime-2.1"andvoice.model: "openai/gpt-5.6-sol". - Realtime voice modes include small
IDENTITY.md,USER.md, andSOUL.mdprofile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Setvoice.realtime.bootstrapContextFilesto a subset to customize this, or[]to disable it. Only those profile files are supported;AGENTS.mdstays in the normal agent context. The injected profile context does not replaceopenclaw_agent_consultfor workspace work, current facts, memory lookup, or tool-backed actions. - In OpenAI
agent-proxyrealtime mode, wake-name gating adapts to the room by default: one human can talk naturally without a wake name, while two or more humans must start or end a turn with one. Other bots do not count as people. Setvoice.realtime.requireWakeName: trueto always require a wake name orfalseto never require one. Configured wake names must be one or two words. Ifvoice.realtime.wakeNamesis unset, OpenClaw uses the routed agentnameplusOpenClaw, falling back to the agent id plusOpenClaw. An active wake-name gate disables realtime provider auto-response, routes accepted turns through the OpenClaw agent consult path, and gives a short spoken acknowledgement when a leading wake name is recognized from partial transcription before the final transcript arrives. The policy follows live joins and leaves without reconnecting voice. - The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio.
voice.realtime.bargeIncontrols whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting.voice.realtime.minBargeInAudioEndMscontrols the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default:250. Set0for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups.- For voice playback only,
voice.ttstakes precedence overttsin place ofstt-tts; realtime modes rely onvoice.realtime.speakerVoiceinstead. When setting up an OpenAI voice for Discord playback, configurevoice.tts.provider: "openai"and pick a Text-to-speech voice fromvoice.tts.providers.openai.speakerVoice. On the current OpenAI TTS model,cedarworks well as a masculine-sounding option. - Voice transcript turns for a given voice channel respect per-channel Discord
systemPromptoverrides. - Upon OpenClaw joining a voice channel, the routed agent session gets a silent system event carrying the current participant roster. Subsequent participant joins or departures refresh that session without prompting an unsolicited spoken reply; Discord display names are considered untrusted labels. Authorized voice turns also receive an updated roster snapshot.
- Voice transcript turns and
/vccommands consult Discord entries incommands.ownerAllowFromfor owner status. If no Discord command owner is set, the selected Discord account'sallowFrom(or legacydm.allowFrom) can still authorize voice access without conferring owner status. Agent tool visibility follows the configured tool policy for the routed session. - When
voice.autoJoinlists multiple entries for the same guild, OpenClaw connects to the last configured channel for that guild. voice.autoJoin[].whenOccupiedcomes set tofalse. Switch it totruefor an auto-managed room that holds only the bot while at least one human is present. OpenClaw connects when the first human arrives and disconnects after the last human leaves; the OpenClaw bot and other bots are excluded from the count. Startup, fresh gateway sessions, and resumed gateway sessions reconcile from Discord's voice-state roster.- Occupancy management only controls sessions it initiated. A manual
/vc join, transcript capture, follow-user session, active session in another channel, or other ad-hoc join remains untouched when the configured room empties. voice.allowedChannelsserves as an optional residency allowlist. Leaving it unset permits/vc joininto any authorized Discord voice channel. When configured,/vc join, startup auto-join, and bot voice-state moves are limited to the listed{ guildId, channelId }entries. Setting it to an empty array blocks all Discord voice joins. If Discord moves the bot outside the allowlist, OpenClaw exits that channel and reconnects to the configured auto-join target when one exists.voice.daveEncryptionandvoice.decryptionFailureToleranceare forwarded to@discordjs/voicejoin options; the upstream defaults aredaveEncryption=trueanddecryptionFailureTolerance=24.- For Discord voice receive and realtime raw PCM playback, OpenClaw uses the bundled
libopus-wasmcodec. It includes a pinned libopus WebAssembly build and needs no native opus addons. voice.connectTimeoutMssets the initial@discordjs/voiceReady wait for/vc joinand auto-join attempts. Default:30000.voice.reconnectGraceMsdetermines how long OpenClaw waits for a disconnected voice session to start reconnecting before tearing it down. Default:15000.- In
stt-ttsmode, voice playback continues even if another user begins speaking. To prevent feedback loops, OpenClaw skips new voice capture while TTS is playing; wait until playback ends to speak for the next turn. Realtime modes send speaker starts as barge-in signals to the realtime provider. - In realtime modes, speaker echo into an open mic can mimic barge-in and halt playback. For echo-heavy Discord rooms, set
voice.realtime.providers.openai.interruptResponseOnInputAudio: falseto prevent OpenAI from auto-interrupting on input audio. Addvoice.realtime.bargeIn: trueif you still want Discord speaker-start events to interrupt active playback. The OpenAI realtime bridge treats playback truncations shorter thanvoice.realtime.minBargeInAudioEndMsas likely echo/noise and logs them as skipped rather than clearing Discord playback. voice.captureSilenceGraceMscontrols how long OpenClaw waits after Discord indicates a speaker has stopped before finalizing that audio segment for STT. Default:2000; increase it if Discord splits normal pauses into choppy partial transcripts.- When ElevenLabs is the chosen TTS provider, Discord voice playback uses streaming TTS and begins from the provider response stream. Providers lacking streaming support revert to the synthesized temp-file path.
- OpenClaw monitors receive decrypt failures and self-recovers by leaving/rejoining the voice channel after repeated failures within a short window.
- If receive logs repeatedly show
DecryptionFailed(UnencryptedWhenPassthroughDisabled)after an update, gather a dependency report and logs. The bundled@discordjs/voiceline includes the upstream padding fix from discord.js PR #11449, which resolved discord.js issue #11419. The operation was abortedreceive events are normal when OpenClaw finalizes a captured speaker segment; they serve as verbose diagnostics, not warnings.- Verbose Discord voice logs include a bounded one-line STT transcript preview for each accepted speaker segment, so debugging shows both the user side and the agent reply side without dumping unbounded transcript text.
- In
agent-proxymode, forced consult fallback skips likely incomplete transcript fragments such as text ending in...or a trailing connector like "and", plus obvious non-actionable closings like "be right back" or "bye". Logs showforced agent consult skipped reason=...when this prevents a stale queued answer.
Follow users in voice
Choose voice.followUsers when you want the Discord voice bot to stick with one or more known Discord users rather than joining a fixed channel at startup or waiting for /vc join.
{
channels: {
discord: {
voice: {
enabled: true,
followUsersEnabled: true,
followUsers: ["discord:123456789012345678"],
allowedChannels: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
},
],
},
},
},
}
Behavior:
followUsersaccepts raw Discord user IDs anddiscord:<id>values. OpenClaw normalizes both forms before matching voice-state events.followUsersEnableddefaults totruewhenfollowUsersis configured. Set it tofalseto retain the saved list but disable automatic voice following.followUsersaffects voice residency only. It does not grant speaker access or owner authority; configurecommands.ownerAllowFromand guild or channel users and roles separately.- When a followed user joins an allowed voice channel, OpenClaw joins that channel. When the user moves, OpenClaw moves with them. When the active followed user disconnects, OpenClaw leaves.
- If multiple followed users are in the same guild and the active followed user leaves, OpenClaw moves to another tracked followed user's channel before leaving the guild. If several followed users move at once, the latest observed voice-state event wins.
allowedChannelsstill applies. A followed user in a disallowed channel is ignored, and a follow-owned session moves to another followed user or leaves.- OpenClaw reconciles missed voice-state events on startup and at a bounded interval. Reconciliation samples configured guilds and caps REST lookups per run, so very large
followUserslists may take more than one interval to converge. - If Discord or an admin moves the bot while it is following a user, OpenClaw rebuilds the voice session and preserves follow ownership when the destination is allowed. If the bot is moved outside
allowedChannels, OpenClaw leaves and rejoins the configured target when one exists. - DAVE receive recovery may leave and rejoin the same channel after repeated decrypt failures. Follow-owned sessions keep their follow ownership through that recovery path, so a later followed-user disconnect still leaves the channel.
Choose which join mode fits your needs:
- Pick
followUsersfor personal or operator-style setups where the bot should be in the voice channel automatically whenever you are. - Pick
autoJoinfor permanent rooms. IncludewhenOccupied: trueif the bot should only be present while people are in that room; leave it out for voice presence that never turns off. - Pick
/vc joinfor one-time joins or rooms where automatic voice presence would feel out of place.
Discord voice codec details:
- Voice receive logs display
discord voice: opus decoder: libopus-wasm. - For realtime playback, raw 48 kHz stereo PCM gets encoded to Opus using the included
libopus-wasmpackage before packets go to@discordjs/voice. - For file and provider-stream playback, ffmpeg transcodes to raw 48 kHz stereo PCM, and then
libopus-wasmhandles the Opus packet stream sent to Discord.
STT and TTS pipeline:
- Discord PCM capture gets written to a temporary WAV file.
tools.media.audioperforms STT, such asopenai/gpt-4o-mini-transcribe.- The transcript flows through Discord ingress and routing while the response LLM runs with a voice-output policy that hides the agent
ttstool and requests returned text, since Discord voice handles final TTS playback. voice.model, if set, overrides only the response LLM for this voice-channel turn.voice.ttsgets merged overtts; providers that support streaming feed the player directly, otherwise the resulting audio file plays in the joined channel.
Default agent-proxy voice-channel session example:
{
channels: {
discord: {
voice: {
enabled: true,
model: "openai/gpt-5.6-sol",
followUsersEnabled: true,
followUsers: ["123456789012345678"],
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
},
},
},
},
}
Without a voice.agentSession block, each voice channel gets its own routed OpenClaw session. For instance, /vc join channel:234567890123456789 communicates with the session for that Discord voice channel. The realtime model acts only as the voice front end; substantive requests go to the configured OpenClaw agent. If the realtime model produces a final transcript without invoking the consult tool, OpenClaw forces the consult as a fallback so the default still works like talking to the agent.
Legacy STT plus TTS example:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "stt-tts",
model: "openai/gpt-5.4-mini",
tts: {
provider: "openai",
providers: {
openai: {
model: "gpt-4o-mini-tts",
speakerVoice: "cedar",
},
},
},
},
},
},
}
Realtime bidi example:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "bidi",
model: "openai/gpt-5.6-sol",
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
toolPolicy: "safe-read-only",
consultPolicy: "always",
},
},
},
},
}
Voice as an extension of an existing Discord channel session:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "agent-proxy",
model: "openai/gpt-5.6-sol",
agentSession: {
mode: "target",
target: "channel:123456789012345678",
},
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
},
},
},
},
}
In agent-proxy mode, the bot joins the configured voice channel, but OpenClaw agent turns use the target channel's normal routed session and agent. The realtime voice session speaks the returned result back into the voice channel. The supervisor agent can still use normal message tools per its tool policy, including sending a separate Discord message if that's the appropriate action.
While a delegated OpenClaw run is active, new Discord voice transcripts are handled as live run control before another agent turn starts. Phrases like "status", "cancel that", "use the smaller fix", or "when you're done also check tests" get classified as status, cancel, steering, or follow-up input for the active session. Status, cancel, accepted steering, and follow-up outcomes are spoken back into the voice channel so the caller knows whether OpenClaw handled the request.
Useful target forms:
target: "channel:123456789012345678"routes through a Discord text channel session.target: "123456789012345678"is treated as a channel target.target: "dm:123456789012345678"ortarget: "user:123456789012345678"routes through that direct-message session.
Echo-heavy OpenAI Realtime example:
{
channels: {
discord: {
voice: {
enabled: true,
mode: "bidi",
model: "openai/gpt-5.6-sol",
realtime: {
provider: "openai",
model: "gpt-realtime-2.1",
speakerVoice: "cedar",
bargeIn: true,
minBargeInAudioEndMs: 500,
consultPolicy: "always",
providers: {
openai: {
interruptResponseOnInputAudio: false,
},
},
},
},
},
},
}
Use this when the model hears its own Discord playback through an open mic, but you still want to interrupt it by speaking. OpenClaw stops OpenAI from auto-interrupting on raw input audio, while bargeIn: true lets Discord speaker-start events and already-active speaker audio cancel active realtime responses before the next captured turn reaches OpenAI. Very early barge-in signals with audioEndMs below minBargeInAudioEndMs are treated as likely echo/noise and ignored so the model does not cut off at the first playback frame.
Expected voice logs:
- On join:
discord voice: joining ... voiceSession=... supervisorSession=... agentSessionMode=... voiceModel=... realtimeModel=... - On realtime start:
discord voice: realtime bridge starting ... autoRespond=false interruptResponse=false bargeIn=false minBargeInAudioEndMs=... - On speaker audio:
discord voice: realtime speaker turn opened ...,discord voice: realtime input audio started ... outputAudioMs=... outputActive=..., anddiscord voice: realtime speaker turn closed ... chunks=... discordBytes=... realtimeBytes=... interruptedPlayback=... - On skipped stale speech:
discord voice: realtime forced agent consult skipped reason=incomplete-transcript ...orreason=non-actionable-closing ... - On realtime response completion:
discord voice: realtime audio playback finishing reason=completed ... audioMs=... chunks=...; buffered audio can still be playing after this line. - On ordinary playback backpressure:
discord voice: realtime audio playback buffering ... bufferedBytes=...; playback continues when Discord drains the buffered audio. - On playback stop/reset:
discord voice: realtime audio playback stopped reason=... audioMs=... elapsedMs=... chunks=... - On realtime consult:
discord voice: realtime consult requested ... voiceSession=... supervisorSession=... question=... - On agent answer:
discord voice: agent turn answer ... - On queued exact speech:
discord voice: realtime exact speech queued ... queued=... outputAudioMs=... outputActive=..., followed bydiscord voice: realtime exact speech dequeued reason=player-idle ... - On barge-in detection:
discord voice: realtime barge-in detected source=speaker-start ...ordiscord voice: realtime barge-in detected source=active-speaker-audio ..., followed bydiscord voice: realtime barge-in requested reason=... outputAudioMs=... outputActive=... - On realtime interruption:
discord voice: realtime model interrupt requested client:response.cancel reason=barge-in, followed by eitherdiscord voice: realtime model audio truncated client:conversation.item.truncate reason=barge-in audioEndMs=...ordiscord voice: realtime model interrupt confirmed server:response.done status=cancelled ... - On ignored echo/noise:
discord voice: realtime model interrupt ignored client:conversation.item.truncate.skipped reason=barge-in audioEndMs=0 minAudioEndMs=250 - On disabled barge-in:
discord voice: realtime capture ignored during playback (barge-in disabled) ... - On idle playback:
discord voice: realtime barge-in ignored reason=... outputActive=false ... playbackChunks=0
To debug cut-off audio, read the realtime voice logs as a timeline:
- When
realtime audio playback startedappears, assistant audio playback has started on Discord. From that moment onward, the bridge tracks assistant output chunks, Discord PCM bytes, provider realtime bytes, and synthesized audio duration. realtime speaker turn openedsignals that a Discord speaker has become active. If playback is already running andbargeInis turned on,barge-in detected source=speaker-startmay appear next.- The first actual audio frame for that speaker turn is indicated by
realtime input audio started. IfoutputActive=trueor a nonzerooutputAudioMsshows up here, the mic is transmitting input while assistant playback continues. - OpenClaw recorded live speaker audio during active assistant playback when
barge-in detected source=active-speaker-audiooccurs. This helps tell a genuine interruption apart from a Discord speaker-start event that carried no meaningful audio. - A request to the realtime provider to cancel or truncate the ongoing response is represented by
barge-in requested reason=.... It carriesoutputAudioMs,outputActive, andplaybackChunksso you can determine how much assistant audio had played before the interruption hit. - The local Discord playback reset point is
realtime audio playback stopped reason=.... Playback is only considered finished whenplayer-idleshows Discord has consumed the audio; providerresponse.doneand encoder completion by themselves do not indicate completion. Other triggers includebarge-in,provider-clear-audio,forced-agent-consult,stream-close,output-audio-overflow, andsession-close. - A summary of the captured input turn is provided by
realtime speaker turn closed. Whenchunks=0orhasAudio=falseappears, the speaker turn started but no usable audio made it to the realtime bridge. IfinterruptedPlayback=trueshows, that input turn overlapped assistant output and barge-in logic fired.
Fields that are useful:
outputAudioMs: how much assistant audio the realtime provider generated before this log line.audioMs: assistant audio duration OpenClaw tallied before playback halted.elapsedMs: elapsed wall-clock time from playback stream or speaker turn opening to its closing.discordBytes: 48 kHz stereo PCM bytes exchanged with Discord voice.realtimeBytes: provider-format PCM bytes exchanged with the realtime provider.playbackChunks: assistant audio chunks forwarded to Discord for the active response.sinceLastAudioMs: time between the last captured speaker audio frame and the speaker turn closing.
Typical scenarios:
- A quick cut-off with
source=active-speaker-audio, a smalloutputAudioMs, and the same user close by usually means speaker echo is reaching the mic. Try raisingvoice.realtime.minBargeInAudioEndMs, turning the speaker down, using headphones, or enablingvoice.realtime.providers.openai.interruptResponseOnInputAudio: false. - When
source=speaker-startis followed byspeaker turn closed ... hasAudio=false, Discord flagged a speaker start but no audio arrived at OpenClaw. This could be a transient Discord voice event, noise gate behavior, or a client briefly keying the mic. audio playback stopped reason=output-audio-overflowindicates sustained delivery failures exceeded the bounded pending-audio queue. Look at the associatedDiscord realtime audio playback overflowerror and any prior provider or Discord connection diagnostics; normal playback backpressure should not trigger this error.audio playback stopped reason=stream-closewithout a nearby barge-in orprovider-clear-audiomeans the local Discord playback stream ended without warning. Review the preceding provider and Discord player logs.- When
capture ignored during playback (barge-in disabled)appears, OpenClaw deliberately dropped input while assistant audio was playing. Turn onvoice.realtime.bargeInif you want speech to interrupt playback. barge-in ignored ... outputActive=falsemeans Discord or the provider VAD detected speech, but OpenClaw had no active playback to interrupt. Audio should not be cut off in this case.
Credentials resolve per component: LLM route auth for voice.model, STT auth for tools.media.audio, TTS auth for tts/voice.tts, and realtime provider auth for voice.realtime.providers or the provider's standard auth config.
Voice messages
Discord voice messages display a waveform preview and need OGG/Opus audio. OpenClaw generates the waveform on its own, but ffmpeg and ffprobe must be present on the gateway host for inspection and conversion.
- Use a local file path (URLs are not accepted).
- Leave out text content (Discord rejects a payload with both text and a voice message).
- Any audio format works; OpenClaw converts to OGG/Opus when necessary.
message(action="send", channel="discord", target="channel:123", path="/path/to/audio.mp3", asVoice=true)
Troubleshooting
Used disallowed intents or bot sees no guild messages
- enable Message Content Intent
- enable Server Members Intent when you rely on user/member resolution
- restart gateway after changing intents
Guild messages blocked unexpectedly
-
confirm
groupPolicy -
confirm the guild allowlist under
channels.discord.guilds -
when a guild
channelsmap is present, only the channels it lists are permitted -
confirm
requireMentionbehavior and how mentions are handledChecks that are useful:
openclaw doctor
openclaw channels status --probe
openclaw logs --follow
Require mention false but still blocked
What usually causes it:
groupPolicy="allowlist"with no matching guild or channel allowlistrequireMentionplaced in the wrong location, it has to sit underchannels.discord.guildsor inside a channel entry- the sender is not on the guild or channel
usersallowlist
Long-running Discord turns or duplicate replies
Logs you would typically see:
Slow listener detected ...stuck session: sessionKey=agent:...:discord:... state=processing ...
Queued agent turns are not subject to a channel-level timeout in Discord. Message listeners hand off right away, and queued Discord runs keep per-session ordering intact until the session, tool, or runtime lifecycle finishes or cancels the work.
Gateway metadata lookup timeout warnings
Before connecting, OpenClaw pulls Discord /gateway/bot metadata. If transient failures occur, the default Discord gateway URL is used as a fallback, and those failures show up rate-limited in the logs.
The metadata timeout is 30 seconds by default. For unusual host environments, OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS can change it.
Gateway READY timeout restarts
During startup and after runtime reconnects, OpenClaw waits for Discord's gateway READY event. Setups with multiple accounts and staggered startup may require a longer startup READY window than the default.
Startup waits 15 seconds, while runtime reconnects wait 30 seconds. OPENCLAW_DISCORD_READY_TIMEOUT_MS and OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS are still there for unusual host environments.
Permissions audit mismatches
channels status --probe permission checks work only with numeric channel IDs.
If slug keys are used, runtime matching may still function, but the probe cannot fully verify permissions.
DM and pairing issues
- DM disabled:
channels.discord.dm.enabled=false - DM policy disabled:
channels.discord.dmPolicy="disabled"(legacy:channels.discord.dm.policy) - waiting for pairing approval in
pairingmode
Bot to bot loops
By default, messages from bots are ignored.
When channels.discord.allowBots=true is set, strict mention and allowlist rules are required to prevent loop behavior.
Prefer channels.discord.allowBots="mentions" so only bot messages that mention the bot are accepted.
OpenClaw also includes shared bot loop protection. Whenever allowBots allows bot-authored messages to reach dispatch, Discord maps the inbound event to (account, channel, bot pair) facts, and the generic pair guard suppresses the pair once it exceeds the configured event budget. This guard stops runaway two-bot loops that previously had to be halted by Discord rate limits; it does not affect single-bot deployments or one-shot bot replies that stay within the budget.
Default settings, active when allowBots is set:
-
maxEventsPerWindow: 20: the bot pair can exchange 20 messages within the sliding window -
windowSeconds: 60: the length of the sliding window -
cooldownSeconds: 60: once the budget trips, any further bot-to-bot message in either direction is dropped for one minuteSet the shared default once under
channels.defaults.botLoopProtection, then override Discord when a legitimate workflow needs more headroom. The order of precedence is: -
channels.discord.accounts.<account>.botLoopProtection -
channels.discord.botLoopProtection -
channels.defaults.botLoopProtection -
built-in defaults
Discord relies on the generic
maxEventsPerWindow,windowSeconds, andcooldownSecondskeys.
{
channels: {
defaults: {
botLoopProtection: {
maxEventsPerWindow: 20,
windowSeconds: 60,
cooldownSeconds: 60,
},
},
discord: {
// Optional Discord-wide override. Account blocks override individual
// fields and inherit omitted fields from here.
botLoopProtection: {
maxEventsPerWindow: 4,
},
accounts: {
alpha: {
// Alpha listens to other bots only when they mention it.
allowBots: "mentions",
},
bravo: {
// Bravo listens to all bot-authored Discord messages.
allowBots: true,
mentionAliases: {
// Lets Bravo write an Alpha Discord mention with the configured user id.
Alpha: "ALPHA_DISCORD_USER_ID",
},
botLoopProtection: {
// Allow up to five messages per minute before suppressing the pair.
maxEventsPerWindow: 5,
windowSeconds: 60,
cooldownSeconds: 90,
},
},
},
},
},
}
Voice STT drops with DecryptionFailed(...)
- keep OpenClaw updated (
openclaw update) so the Discord voice receive recovery logic is included - verify
channels.discord.voice.daveEncryption=true(default) - begin with
channels.discord.voice.decryptionFailureTolerance=24(upstream default) and adjust only when necessary - monitor logs for:
discord voice: DAVE decrypt failures detecteddiscord voice: repeated decrypt failures; attempting rejoin
- if failures persist after automatic rejoin, gather logs and compare them against the upstream DAVE receive history in discord.js #11419 and discord.js #11449
Configuration reference
Main reference: Configuration reference - Discord.
High-signal Discord fields
- startup/auth:
enabled,token,applicationId,accounts.*,allowBots - policy:
groupPolicy,dmPolicy,allowFrom,dm.*,guilds.*,guilds.*.channels.* - group introductions:
joinIntro,accounts.*.joinIntro(default:true) - command:
commands.native,commands.allowFrom(global),configWrites,slashCommand.ephemeral - gateway:
proxy - reply/history:
replyToMode,historyLimit,dmHistoryLimit,dms.*.historyLimit - delivery:
textChunkLimit(default2000),maxLinesPerMessage(default17) - streaming:
streaming.mode,streaming.chunkMode,streaming.preview.*,streaming.progress.*,streaming.block.*(legacy flatstreamMode,draftChunk,blockStreaming,blockStreamingCoalesce,chunkModekeys are migrated intostreaming.*byopenclaw doctor --fix) - media:
mediaMaxMb(caps outbound Discord uploads, default100) - actions:
actions.* - presence:
activity,status,activityType,activityUrl,autoPresence.* - features:
threadBindings, top-levelbindings[](type: "acp"),pluralkit,execApprovals,intents,agentComponents.enabled,agentComponents.ttlMs,activities,heartbeatVisibility,responsePrefix
Discord Activities
Set channels.discord.activities to enable the core show_widget tool to generate self-contained HTML widgets that open within Discord. This block is optional. Discord registers the Activity plumbing statically, yet the current-channel presenter remains unavailable, and /discord/activity stays externally hidden behind the standard 404 until an enabled account has a bot token, a resolved client secret, and an application ID. For the Developer Portal, tunnel, security, and troubleshooting setup, see Discord Activities.
activities.clientSecret: OAuth2 client secret for the Discord application; falls back toDISCORD_CLIENT_SECRETactivities.applicationId: optional Activity application ID; defaults to the bot application ID learned at gateway startup
Safety and operations
- Bot tokens should be treated as secrets (
DISCORD_BOT_TOKENis preferred in supervised environments). - Apply least-privilege Discord permissions.
- If command deploy/state is stale, restart the gateway and verify with
openclaw channels status --probe.
Related
-
Discord Activities, Launch interactive HTML widgets inside Discord.
-
Pairing, Pair a Discord user to the gateway.
-
Groups, Group chat and allowlist behavior.
-
Channel routing, Route inbound messages to agents.
-
Security, Threat model and hardening.
-
Multi-agent routing, Map guilds and channels to agents.
-
Slash commands, Native command behavior.