---
summary: "How OpenClaw picks an agent and session for each inbound message, and how replies route back"
read_when:
  - Changing channel routing or inbox behavior
title: "Channel routing"
---

# Channels & routing

OpenClaw routes replies **back to the channel where a message came from**. The
model does not choose a channel; routing follows fixed rules controlled by the
host configuration. Under the default DM scope, direct messages from every
channel converge on the agent's [main session](/concepts/main-session).

## Key terms

- **Channel**: a channel plugin such as `discord`, `googlechat`, `imessage`, `irc`, `line`, `signal`, `slack`, `telegram`, or `whatsapp`. `webchat` is the internal WebChat UI channel and is not a configurable outbound channel.
- **AccountId**: per-channel account instance (when supported).
- Optional channel default account: `channels.<channel>.defaultAccount` chooses
  which account is used when an outbound path does not specify `accountId`.
  - In multi-account setups, set an explicit default (`defaultAccount` or an account named `default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID.
- **AgentId**: an isolated workspace + session store ("brain").
- **SessionKey**: the bucket key used to store context and control concurrency.

## Outbound target prefixes

Explicit outbound targets may include a provider prefix, such as `telegram:123` or `tg:123`. Core treats that prefix as a channel-selection hint only when the selected channel is `last` or otherwise unresolved, and only when the loaded plugin advertises that prefix. If the caller already selected an explicit channel, the provider prefix must match that channel; cross-channel combinations such as WhatsApp delivery to `telegram:123` fail before plugin-specific target normalization.

Target-kind and service prefixes such as `channel:<id>`, `user:<id>`, `room:<id>`, `thread:<id>`, `imessage:<handle>`, and `sms:<number>` stay inside the selected channel's grammar. They do not select the provider by themselves.

Plugin send receipts that report failure, suppression, or dry run leave the
conversation's stored route and delivery transcript unchanged. Confirmed partial
sends can establish a route, but requested content is not mirrored as fully
delivered.

## Session key shapes (examples)

Direct messages collapse to the agent's **main** session by default:

- `agent:<agentId>:main` (for example: `agent:main:main`)

`session.dmScope` controls DM collapsing: `main` (default) shares one main
session, while `per-peer`, `per-channel-peer`, and `per-account-channel-peer`
keep DMs in separate sessions. A route binding can override the scope for its
matched peers via `bindings[].session.dmScope`.

Even when direct-message conversation history is shared with main, sandbox and
tool policy use a derived per-account direct-chat runtime key for external DMs
so channel-originated messages are not treated like local main-session runs.

With the default `session.groupScope: "per-group"`, groups and channels remain
isolated per channel:

- Groups: `agent:<agentId>:<channel>:group:<id>`
- Channels/rooms: `agent:<agentId>:<channel>:channel:<id>`

Set `session.groupScope: "main"` to route all non-direct peers into the agent's
main session, or use `bindings[].session.groupScope` for selected rooms. The
binding override wins over the global value. This changes shared context only;
mention gating and replies still use the originating 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:42`
- `agent:main:discord:channel:123456:thread:987654`

## Main DM route pinning

When `session.dmScope` is `main`, direct messages may share one main session.
To prevent the session's `lastRoute` from being overwritten by non-owner DMs,
OpenClaw infers a pinned owner from `allowFrom` when all of these are true:

- `allowFrom` has exactly one non-wildcard entry.
- The entry can be normalized to a concrete sender ID for that channel.
- The inbound DM sender does not match that pinned owner.

In that mismatch case, OpenClaw still records inbound session metadata, but it
skips updating the main session `lastRoute`.

## Guarded inbound recording

Channel plugins can mark an inbound session record as `createIfMissing: false`
when a guarded path must not create a new OpenClaw session. In that mode,
OpenClaw may update metadata and `lastRoute` for an existing session, but it
does not create a route-only session entry just because a message was observed.

## Routing rules (how an agent is chosen)

Ordinary routing picks **one agent** for each inbound message:

1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`).
2. **Parent peer match** (thread inheritance).
3. **Peer wildcard match** (`peer.id: "*"` for a peer kind).
4. **Guild + roles match** (Discord) via `guildId` + `roles`.
5. **Guild match** (Discord) via `guildId`.
6. **Team match** (Slack) via `teamId`.
7. **Account match** (`accountId` on the channel).
8. **Channel match** (any account on that channel, `accountId: "*"`).
9. **Fallback owner**: an owner supplied by the caller, otherwise the sole configured agent or a retained legacy owner. Multiple agents without an owner require a matching binding; routing does not pick the first roster entry.

Run `openclaw doctor --fix` to migrate legacy default markers before startup. Fresh configurations without an agent roster receive the single `main` agent.

When a binding includes multiple match fields (`peer`, `guildId`, `teamId`, `roles`), **all provided fields must match** for that binding to apply.

The matched agent determines which workspace and session store are used.

## Broadcast groups (run multiple agents)

Agent group threads use the top-level `broadcast` config to run several agents
for an admitted inbound message. A qualified `"<channel>:<peerId>"` key takes
precedence over an unqualified WhatsApp peer key. Ordinary routing still
provides the conversation route; the coordinator gives each participant its
own agent session for that channel, account, peer, and thread.

```json5
{
  broadcast: {
    strategy: "parallel",
    "telegram:-100123": {
      agents: ["reviewer", "writer"],
      maxRounds: 2,
      maxTurns: 4,
    },
    "slack:C0123": ["support", "reviewer"],
    "120363403215116621@g.us": ["alfred", "baerbel"],
  },
}
```

Qualified entries default to explicit mention selection, one round, and one
turn per configured agent. `maxTurns` bounds participant runs started across
all rounds, not physical platform messages. Legacy WhatsApp arrays retain
single-pass fan-out to all listed agents.

Channel allowlists still apply. On Discord, Slack, and Telegram, an explicit
mention of any qualified participant can satisfy the room’s mention gate.
Configured ACP bindings remain exclusive and bypass group-thread fan-out.

See [Broadcast groups](/channels/broadcast-groups) for selection, continuation
eligibility, budgets, and participant labeling. Control UI does not yet offer a
dedicated team-thread session.

## Config overview

- `agents.entries`: named agent definitions (workspace, model, etc.).
- `bindings`: map inbound channels/accounts/peers to agents.

Example:

```json5
{
  agents: {
    entries: {
      support: {
        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

Runtime session rows and transcripts live in each agent's SQLite database under
the state directory (default `~/.openclaw`):

- `~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite`

Older installs may have legacy transcript JSONL files and a `sessions.json` row
store under `~/.openclaw/agents/<agentId>/sessions/`. To import that history into
SQLite, stop the Gateway, back up its state, and run `openclaw doctor --fix`
before restarting it. Gateway startup does not import legacy session files: if
it finds a legacy store, it refuses readiness and prints the Doctor command for
the active profile. Use `openclaw doctor --session-sqlite inspect
--session-sqlite-all-agents` and the
[Doctor](/cli/doctor#session-sqlite-migration) migration sequence for inspection
and validation.

`session.store` supports `{agentId}` templating. At runtime, a legacy store path
selects its corresponding SQLite database; the JSON file itself is only a
migration input or an explicit offline-maintenance target.

Gateway session discovery can include on-disk stores under the default `agents/`
root and templated `session.store` roots that use the
`agents/<agentId>/sessions/sessions.json` layout. It recognizes the corresponding
`agent/openclaw-agent.sqlite` database without requiring a legacy `sessions.json`
file. Discovered store files must be regular files within the resolved agent
root; symlinked store files and out-of-root paths are ignored.

ACP session discovery reads SQLite ACP metadata and joins it to the corresponding
session entries.

## WebChat behavior

WebChat attaches to the **selected agent** and defaults to the agent's main
session. Because of this, WebChat lets you see cross-channel context for that
agent in one place.

A WebChat turn and its background completions stay in WebChat unless an external
route is explicitly selected. The session's saved external route is retained for
callers that intentionally use it; it does not override the current turn's origin.
Internally triggered media tasks can use a WebChat channel sentinel without an
interactive WebChat origin; their media admission still pins the saved external
route.

An outbound `send` request can set `sessionKey` to mirror delivered output into
a different transcript. This does not rebind that transcript's delivery route:
the resolved destination conversation owns the saved route, including when
channel routing intentionally selects a shared main session.

## Reply context

Inbound replies include:

- `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available.
- Quoted context is appended to `Body` as a `[Replying to ...]` block.

This is consistent across channels.

## Related

- [Groups](/channels/groups)
- [Broadcast groups](/channels/broadcast-groups)
- [Pairing](/channels/pairing)
- [Multi-agent routing](/concepts/multi-agent)
- [Agent bindings](/concepts/agent-bindings)
