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.

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:

  • bot
  • applications.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:

  1. User Settings (gear icon) → Developer → enable Developer Mode (on mobile: App SettingsAdvanced)
  2. Right-click your server iconCopy Server ID
  3. Right-click your own avatarCopy 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 iconPrivacy 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_TOKEN applies 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 reason duplicate bot token. For advanced outbound calls (message tool/channel actions), an explicit per-call token is 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.

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 passing CommandTargetSessionKey to 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 create to 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:

  • file blocks must reference an attachment (attachment://<filename>)
  • Supply the attachment via media/path/filePath (single file); use media-gallery for multiple files
  • Override the upload name with filename when it should align with the attachment reference

Modal forms:

  • Add components.modal with 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 one allowFrom sender)
  • open (requires channels.discord.allowFrom to 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 dmPolicy and groupPolicy are omitted, they take on the channel root. Explicit account policies override; if neither scope is set, the defaults remain pairing and allowlist in that order.
  • Only the default account is affected by channels.discord.accounts.default.allowFrom.
  • For a single account, allowFrom outranks the older dm.allowFrom.
  • Named accounts fall back to channels.discord.allowFrom when their own allowFrom and legacy dm.allowFrom aren'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

  • disabled

    When channels.discord is present, the secure baseline is allowlist.

    allowlist behavior:

  • guild must match channels.discord.guilds (id preferred, slug accepted)

  • optional sender allowlists: users (stable IDs recommended) and roles (role IDs only); if either is set, senders are allowed when they match users OR roles

  • direct name/tag matching is off by default; turn on channels.discord.dangerouslyAllowNameMatching: true only as break-glass compatibility mode

  • names/tags work for users, but IDs are safer; openclaw security audit warns when name/tag entries appear

  • if a guild has channels configured, channels not listed are denied

  • if a guild lacks a channels block, 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, with messages.groupChat.mentionPatterns as 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.native has a default of "auto" and is turned on for Discord.
  • To override per channel, use channels.discord.commands.native.
  • commands.native=false bypasses 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 work
  • first: adds the implicit native reply reference to the first outbound Discord message of the turn
  • all: adds it to every outbound message
  • batched: 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.

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,
        },
      },
    },
  },
}
  • off disables Discord preview edits.
  • partial edits a single preview message as tokens arrive.
  • block emits draft-sized chunks; tune size and breakpoints with streaming.preview.chunk (minChars, maxChars, breakPreference), clamped to textChunkLimit. An explicit non-off preview mode overrides inherited agents.defaults.blockStreamingDefault: "on"; explicit streaming.block.enabled: true overrides the preview. If a turn cannot use previews, inherited block delivery still applies.
  • progress keeps 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.toolProgress and streaming.progress.toolProgress both default to true when preview streaming is active. Tool rows such as 🛠️ Bash: run tests or 🔎 Web Search: for "query" need no additional progress config; set either key to false to keep the status headline only.
  • streaming.progress.commentary (default false) 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.maxLineChars controls the per-line progress preview budget. Prose is shortened on word boundaries; command and path details keep useful suffixes.
  • streaming.preview.commandText / streaming.progress.commandText controls command/exec detail in compact progress lines: status (default, tool label only) or raw (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.historyLimit default 20
  • fallback: messages.groupChat.historyLimit
  • 0 disables

DM history controls:

  • channels.discord.dmHistoryLimit
  • channels.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 /model selection as a model-only fallback; thread-local /model selections take precedence, and parent transcript history is not copied unless transcript inheritance is enabled.
  • channels.discord.thread.inheritParent (default false) 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: false is 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
  • /unfocus remove current thread binding
  • /agents show 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 by spawnSessions, which defaults to true.

  • Native subagent context for thread-bound spawns is managed by defaultSpawnContext, with "fork" as its default.

  • The deprecated spawnSubagentSessions and spawnAcpSessions keys undergo migration via openclaw doctor --fix.

  • When thread bindings are turned off, /focus and 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, /new and /reset reset that ACP session in place. While active, temporary thread bindings can override how targets are resolved.

  • Child thread creation and binding is gated by spawnSessions through --thread auto|here.

    Binding behavior specifics are covered in ACP Agents.

Reaction notifications

Per-guild reaction notification mode (guilds.<id>.reactionNotifications):

  • off
  • own (the default)
  • all
  • allowlist (relies on guilds.<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>.ackReaction
  • channels.discord.ackReaction
  • messages.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, assign messages.ackReactionScope the 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: true is 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 allowBots permits 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; activityUrl in turn needs activityType: 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.enabled
  • channels.discord.execApprovals.approvers (optional; falls back to commands.ownerAllowFrom when 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 groupDefault
reactions, messages, threads, pins, polls, search, memberInfo, roleInfo, channelInfo, channels, voiceStatus, events, stickers, emojiUploads, stickerUploads, permissionsenabled
rolesdisabled
moderationdisabled
presencedisabled

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.ttlMs determines how long sent Discord component callbacks stay registered (default 1800000, max 86400000). Per account: channels.discord.accounts.<id>.agentComponents.ttlMs.
  • When components v2 are present, embeds are disregarded.
  • URL previews are turned off by default. Set suppressEmbeds: false on 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:

  1. Turn on Message Content Intent in the Discord Developer Portal.
  2. Turn on Server Members Intent when role or user allowlists are in use.
  3. Invite the bot with the bot and applications.commands scopes.
  4. In the target voice channel, grant Connect, Speak, Send Messages, and Read Message History.
  5. Enable native commands (commands.native or channels.discord.commands.native).
  6. 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-proxy response and wake-name policies below require a GA realtime model, such as gpt-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 existing channels.discord.voice block) to activate /vc commands, the voice runtime, and the GuildVoiceStates gateway intent. channels.discord.intents.voiceStates can explicitly override the intent subscription; leave it unset to follow effective voice enablement.
  • The conversation path is governed by voice.mode. The default is agent-proxy: a realtime voice front end manages turn timing, interruption, and playback, delegates substantive work to the routed OpenClaw agent via openclaw_agent_consult, and treats the result like a typed Discord prompt from that speaker. stt-tts preserves the older batch STT plus TTS flow. bidi lets the realtime model converse directly while exposing openclaw_agent_consult for 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.model overrides the OpenClaw agent brain for Discord voice responses and realtime consults. Leave it unset to inherit the routed agent model. It is distinct from voice.realtime.model.
  • voice.followUsers allows the bot to join, move, and leave Discord voice with selected users. See Follow users in voice.
  • Speech is routed through agent-proxy via discord-voice, which keeps normal owner/tool authorization for the speaker and target session but hides the agent tts tool because Discord voice owns playback. By default, agent-proxy grants 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 default always mode, 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 terminally log, then use /vc join to reconnect. Temporary provider reconnects do not end the Discord voice session.
  • In stt-tts mode, STT uses tools.media.audio; voice.model does not affect transcription.
  • stt-tts replies 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, and voice.realtime.speakerVoice configure the realtime audio session. For OpenAI Realtime 2.1 plus the Codex brain, use voice.realtime.model: "gpt-realtime-2.1" and voice.model: "openai/gpt-5.6-sol".
  • Realtime voice modes include small IDENTITY.md, USER.md, and SOUL.md profile 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. Set voice.realtime.bootstrapContextFiles to a subset to customize this, or [] to disable it. Only those profile files are supported; AGENTS.md stays in the normal agent context. The injected profile context does not replace openclaw_agent_consult for workspace work, current facts, memory lookup, or tool-backed actions.
  • In OpenAI agent-proxy realtime 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. Set voice.realtime.requireWakeName: true to always require a wake name or false to never require one. Configured wake names must be one or two words. If voice.realtime.wakeNames is unset, OpenClaw uses the routed agent name plus OpenClaw, falling back to the agent id plus OpenClaw. 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.bargeIn controls whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting.
  • voice.realtime.minBargeInAudioEndMs controls the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default: 250. Set 0 for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups.
  • For voice playback only, voice.tts takes precedence over tts in place of stt-tts; realtime modes rely on voice.realtime.speakerVoice instead. When setting up an OpenAI voice for Discord playback, configure voice.tts.provider: "openai" and pick a Text-to-speech voice from voice.tts.providers.openai.speakerVoice. On the current OpenAI TTS model, cedar works well as a masculine-sounding option.
  • Voice transcript turns for a given voice channel respect per-channel Discord systemPrompt overrides.
  • 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 /vc commands consult Discord entries in commands.ownerAllowFrom for owner status. If no Discord command owner is set, the selected Discord account's allowFrom (or legacy dm.allowFrom) can still authorize voice access without conferring owner status. Agent tool visibility follows the configured tool policy for the routed session.
  • When voice.autoJoin lists multiple entries for the same guild, OpenClaw connects to the last configured channel for that guild.
  • voice.autoJoin[].whenOccupied comes set to false. Switch it to true for 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.allowedChannels serves as an optional residency allowlist. Leaving it unset permits /vc join into 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.daveEncryption and voice.decryptionFailureTolerance are forwarded to @discordjs/voice join options; the upstream defaults are daveEncryption=true and decryptionFailureTolerance=24.
  • For Discord voice receive and realtime raw PCM playback, OpenClaw uses the bundled libopus-wasm codec. It includes a pinned libopus WebAssembly build and needs no native opus addons.
  • voice.connectTimeoutMs sets the initial @discordjs/voice Ready wait for /vc join and auto-join attempts. Default: 30000.
  • voice.reconnectGraceMs determines how long OpenClaw waits for a disconnected voice session to start reconnecting before tearing it down. Default: 15000.
  • In stt-tts mode, 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: false to prevent OpenAI from auto-interrupting on input audio. Add voice.realtime.bargeIn: true if you still want Discord speaker-start events to interrupt active playback. The OpenAI realtime bridge treats playback truncations shorter than voice.realtime.minBargeInAudioEndMs as likely echo/noise and logs them as skipped rather than clearing Discord playback.
  • voice.captureSilenceGraceMs controls 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/voice line includes the upstream padding fix from discord.js PR #11449, which resolved discord.js issue #11419.
  • The operation was aborted receive 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-proxy mode, 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 show forced 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:

  • followUsers accepts raw Discord user IDs and discord:<id> values. OpenClaw normalizes both forms before matching voice-state events.
  • followUsersEnabled defaults to true when followUsers is configured. Set it to false to retain the saved list but disable automatic voice following.
  • followUsers affects voice residency only. It does not grant speaker access or owner authority; configure commands.ownerAllowFrom and 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.
  • allowedChannels still 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 followUsers lists 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 followUsers for personal or operator-style setups where the bot should be in the voice channel automatically whenever you are.
  • Pick autoJoin for permanent rooms. Include whenOccupied: true if the bot should only be present while people are in that room; leave it out for voice presence that never turns off.
  • Pick /vc join for 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-wasm package before packets go to @discordjs/voice.
  • For file and provider-stream playback, ffmpeg transcodes to raw 48 kHz stereo PCM, and then libopus-wasm handles the Opus packet stream sent to Discord.

STT and TTS pipeline:

  • Discord PCM capture gets written to a temporary WAV file.
  • tools.media.audio performs STT, such as openai/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 tts tool 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.tts gets merged over tts; 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" or target: "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=..., and discord voice: realtime speaker turn closed ... chunks=... discordBytes=... realtimeBytes=... interruptedPlayback=...
  • On skipped stale speech: discord voice: realtime forced agent consult skipped reason=incomplete-transcript ... or reason=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 by discord voice: realtime exact speech dequeued reason=player-idle ...
  • On barge-in detection: discord voice: realtime barge-in detected source=speaker-start ... or discord voice: realtime barge-in detected source=active-speaker-audio ..., followed by discord 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 either discord voice: realtime model audio truncated client:conversation.item.truncate reason=barge-in audioEndMs=... or discord 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:

  1. When realtime audio playback started appears, 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.
  2. realtime speaker turn opened signals that a Discord speaker has become active. If playback is already running and bargeIn is turned on, barge-in detected source=speaker-start may appear next.
  3. The first actual audio frame for that speaker turn is indicated by realtime input audio started. If outputActive=true or a nonzero outputAudioMs shows up here, the mic is transmitting input while assistant playback continues.
  4. OpenClaw recorded live speaker audio during active assistant playback when barge-in detected source=active-speaker-audio occurs. This helps tell a genuine interruption apart from a Discord speaker-start event that carried no meaningful audio.
  5. A request to the realtime provider to cancel or truncate the ongoing response is represented by barge-in requested reason=.... It carries outputAudioMs, outputActive, and playbackChunks so you can determine how much assistant audio had played before the interruption hit.
  6. The local Discord playback reset point is realtime audio playback stopped reason=.... Playback is only considered finished when player-idle shows Discord has consumed the audio; provider response.done and encoder completion by themselves do not indicate completion. Other triggers include barge-in, provider-clear-audio, forced-agent-consult, stream-close, output-audio-overflow, and session-close.
  7. A summary of the captured input turn is provided by realtime speaker turn closed. When chunks=0 or hasAudio=false appears, the speaker turn started but no usable audio made it to the realtime bridge. If interruptedPlayback=true shows, 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 small outputAudioMs, and the same user close by usually means speaker echo is reaching the mic. Try raising voice.realtime.minBargeInAudioEndMs, turning the speaker down, using headphones, or enabling voice.realtime.providers.openai.interruptResponseOnInputAudio: false.
  • When source=speaker-start is followed by speaker 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-overflow indicates sustained delivery failures exceeded the bounded pending-audio queue. Look at the associated Discord realtime audio playback overflow error and any prior provider or Discord connection diagnostics; normal playback backpressure should not trigger this error.
  • audio playback stopped reason=stream-close without a nearby barge-in or provider-clear-audio means 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 on voice.realtime.bargeIn if you want speech to interrupt playback.
  • barge-in ignored ... outputActive=false means 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 channels map is present, only the channels it lists are permitted

  • confirm requireMention behavior and how mentions are handled

    Checks 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 allowlist
  • requireMention placed in the wrong location, it has to sit under channels.discord.guilds or inside a channel entry
  • the sender is not on the guild or channel users allowlist

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 pairing mode

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 minute

    Set 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, and cooldownSeconds keys.

{
  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 detected
    • discord 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 (default 2000), maxLinesPerMessage (default 17)
  • streaming: streaming.mode, streaming.chunkMode, streaming.preview.*, streaming.progress.*, streaming.block.* (legacy flat streamMode, draftChunk, blockStreaming, blockStreamingCoalesce, chunkMode keys are migrated into streaming.* by openclaw doctor --fix)
  • media: mediaMaxMb (caps outbound Discord uploads, default 100)
  • actions: actions.*
  • presence: activity, status, activityType, activityUrl, autoPresence.*
  • features: threadBindings, top-level bindings[] (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 to DISCORD_CLIENT_SECRET
  • activities.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_TOKEN is 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.
11,130 words · updated Sep 1, 2026