Channel Routing Rules for WhatsApp, Telegram, Discord, Slack
Learn how OpenClaw routes messages across channels like WhatsApp, Telegram, Discord, and Slack. Understand key terms and outbound target prefixes for configuring routing.
Read this when
- Changing channel routing or inbox behavior
Channels & routing
When a message arrives, OpenClaw sends the reply back to the originating channel. The model has no say in this; routing follows a fixed rule set defined by the host configuration. With the default DM scope, direct messages from all channels funnel into the agent's main session.
Key terms
- Channel: a channel plugin like
discord,googlechat,imessage,irc,line,signal,slack,telegram, orwhatsapp.webchatserves as the internal WebChat UI channel and cannot be configured as an outbound channel. - AccountId: the per-channel account instance, where supported.
- Optional channel default account:
channels.<channel>.defaultAccountdetermines which account gets used when an outbound path omitsaccountId.- In multi-account setups, define an explicit default (
defaultAccountor an account nameddefault) when more than one account exists. Without one, fallback routing might select the first normalized account ID.
- In multi-account setups, define an explicit default (
- AgentId: an isolated workspace plus session store, effectively a "brain".
- SessionKey: the bucket key holding context and governing concurrency.
Outbound target prefixes
Explicit outbound targets can carry a provider prefix like telegram:123 or tg:123. Core treats that prefix as a channel-selection hint only when the chosen channel is last or otherwise ambiguous, and only if the loaded plugin advertises that prefix. When the caller has already fixed an explicit channel, the provider prefix must align with it; mismatched combinations, such as WhatsApp delivery to telegram:123, fail before plugin-specific target normalization runs.
Target-kind and service prefixes, including channel:<id>, user:<id>, room:<id>, thread:<id>, imessage:<handle>, and sms:<number>, remain within the selected channel's grammar. They never pick the provider on their own.
Session key shapes (examples)
By default, direct messages collapse into the agent's main session:
agent:<agentId>:main(for instance:agent:main:main)
DM collapsing is governed by session.dmScope: main (the default) gives everyone a single main session, whereas per-peer, per-channel-peer, and per-account-channel-peer keep DMs in separate sessions. A route binding can alter the scope for its matched peers through bindings[].session.dmScope.
Even when direct-message history is shared with main, sandbox and tool policy rely on a derived per-account direct-chat runtime key for external DMs, so channel-originated messages are not handled like local main-session runs.
With the default session.groupScope: "per-group", groups and channels stay isolated per channel:
- Groups:
agent:<agentId>:<channel>:group:<id> - Channels/rooms:
agent:<agentId>:<channel>:channel:<id>
Set session.groupScope: "main" to send all non-direct peers into the agent's main session, or apply bindings[].session.groupScope for specific rooms. The binding override takes precedence over the global setting. Only shared context changes; mention gating and replies still use the original group or channel.
Threads:
- Slack/Discord threads append
:thread:<threadId>to the base key. - Telegram forum topics embed
:topic:<topicId>in the group key.
Examples:
agent:main:telegram:group:-1001234567890:topic:42agent:main:discord:channel:123456:thread:987654
Main DM route pinning
When session.dmScope equals main, direct messages can share one main session. To stop the session's lastRoute from being overwritten by non-owner DMs, OpenClaw infers a pinned owner from allowFrom when every condition below holds:
allowFromhas exactly one non-wildcard entry.- That entry normalizes to a concrete sender ID for the channel.
- The inbound DM sender does not match the pinned owner.
In that mismatch case, OpenClaw still records inbound session metadata but skips updating the main session lastRoute.
Guarded inbound recording
Channel plugins can flag an inbound session record as createIfMissing: false when a guarded path must not spawn a new OpenClaw session. In this mode, OpenClaw may refresh metadata and lastRoute for an existing session, but it will not create a route-only session entry just because a message was seen.
Routing rules (how an agent is chosen)
Routing assigns one agent to each inbound message:
- Exact peer match (
bindingswithpeer.kind+peer.id). - Parent peer match (thread inheritance).
- Peer wildcard match (
peer.id: "*"for a peer kind). - Guild + roles match (Discord) via
guildId+roles. - Guild match (Discord) via
guildId. - Team match (Slack) via
teamId. - Account match (
accountIdon the channel). - Channel match (any account on that channel,
accountId: "*"). - Default agent (
agents.entries.*.default, else first list entry, fallback tomain).
For a binding to take effect when it carries several match fields (peer, guildId, teamId, roles), every supplied field has to align.
The chosen agent dictates which workspace and session store get used.
Broadcast groups (run multiple agents)
Broadcast groups make it possible to run several agents for the same peer in situations where OpenClaw would otherwise respond (such as WhatsApp groups, following mention/activation gating).
Configuration:
{
broadcast: {
strategy: "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"],
"+15555550123": ["support", "logger"],
},
}
Reference: Broadcast Groups.
Config overview
agents.entries: named agent definitions (workspace, model, etc.).bindings: maps inbound channels/accounts/peers to agents.
Sample:
{
agents: {
entries: {
support: {
default: true,
name: "Support",
workspace: "~/.openclaw/workspace-support",
},
},
},
bindings: [
{ match: { channel: "slack", teamId: "T123" }, agentId: "support" },
{
match: { channel: "slack", peer: { kind: "channel", id: "C0123TEAM" } },
agentId: "support",
session: { groupScope: "main" },
},
],
}
Session storage
Each agent's SQLite database, located in the state directory (default ~/.openclaw), holds runtime session rows and transcripts:
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite
Installs from earlier versions might still have legacy transcript JSONL files and a sessions.json row store under ~/.openclaw/agents/<agentId>/sessions/. To bring that history into SQLite, halt the Gateway, back up its state, and execute openclaw doctor --fix prior to restarting. Gateway startup skips importing legacy session files: when a legacy store is detected, readiness is refused and the Doctor command for the active profile gets printed. For inspection and validation, rely on openclaw doctor --session-sqlite inspect --session-sqlite-all-agents and the Doctor migration sequence.
session.store offers {agentId} templating. At runtime, a legacy store path points to its matching SQLite database; the JSON file serves only as a migration input or an explicit offline-maintenance target.
Gateway session discovery can pick up on-disk stores under the default agents/ root and templated session.store roots that follow the agents/<agentId>/sessions/sessions.json layout. It identifies the corresponding agent/openclaw-agent.sqlite database even without a legacy sessions.json file. Discovered store files must be regular files inside the resolved agent root; symlinked store files and paths outside the root get ignored.
ACP session discovery pulls SQLite ACP metadata and links it to the relevant session entries.
WebChat behavior
WebChat connects to the selected agent and uses the agent's main session by default. Consequently, WebChat offers a single view of cross-channel context for that agent.
Reply context
Inbound replies come with:
ReplyToId,ReplyToBody, andReplyToSenderwhen they exist.- Quoted context gets appended to
Bodyas a[Replying to ...]block.
This behavior holds across all channels.