Multi-Agent Routing: Isolated Agents in One Gateway
Learn how to run multiple isolated agents in a single Gateway process, each with its own workspace, state, and session history. This guide is for developers configuring bindings between channel accounts and agents.
Run several isolated agents inside a single Gateway process. Each one gets its own workspace, state directory (agentDir), and session history backed by SQLite, along with multiple channel accounts, for example two WhatsApp numbers. Bindings decide which agent receives each inbound message.
An agent represents the complete per-persona scope: workspace files, auth profiles, model registry, and session store. A binding links a channel account, such as a Slack workspace or a WhatsApp number, to a specific agent.
For a focused setup guide with account and conversation examples, see Agent bindings.
What is one agent
Each agent comes with its own:
- Workspace: files,
AGENTS.md/SOUL.md/USER.md, local notes, persona rules. - State directory (
agentDir): auth profiles, model registry, per-agent config. - Session store: chat history and routing state in
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite.
Auth profiles are per-agent, read from:
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite
Note
sessions_historyis the safer cross-session recall path: it returns a bounded, redacted view, not a raw transcript dump. It strips thinking-block signatures, tool-result payload details,<relevant-memories>scaffolding, tool-call XML tags (<tool_call>,<function_call>, and their plural/downgraded forms), and MiniMax tool-call XML, then truncates and caps output by byte size.
Warning
Never reuse
agentDiracross agents, as it causes auth/session state collisions. When a secondary agent's local OAuth credential is expired or its refresh fails, OpenClaw reads through to the default/main agent's credential for the same profile id and adopts whichever token is freshest, without copying the refresh token into the secondary agent's store. If you want a fully independent OAuth account, sign in from that agent. If you copy credentials manually, copy only portable staticapi_keyortokenprofiles, as OAuth refresh material is not portable by default (copyToAgentscan opt a profile in explicitly).
Skills load from each agent workspace plus shared roots such as ~/.openclaw/skills, then filter by the effective agent skill allowlist. Use agents.defaults.skills for a shared baseline and agents.entries.*.skills for a per-agent replacement (explicit entries replace the default, they do not merge). See Skills: per-agent vs shared and Skills: agent allowlists.
Plugin-owned storage follows that plugin's configuration; adding a second agent does not automatically split every global plugin store. For example, configure Memory Wiki per-agent vaults when personas must not share compiled wiki knowledge.
Note
Workspace note: each agent's workspace is the default cwd, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See Sandboxing.
Paths
| What | Default | Override |
|---|---|---|
| Config | ~/.openclaw/openclaw.json | OPENCLAW_CONFIG_PATH |
| State dir | ~/.openclaw | OPENCLAW_STATE_DIR |
| Default agent's workspace | <stateDir>/workspace (~/.openclaw-<profile>/workspace for a named profile) | agents.entries.*.workspace, then agents.defaults.workspace, or OPENCLAW_WORKSPACE_DIR |
| Other agents' workspace | <stateDir>/workspace-<agentId> (or <agents.defaults.workspace>/<agentId> when set) | agents.entries.*.workspace |
| Agent dir | ~/.openclaw/agents/<agentId>/agent | agents.entries.*.agentDir |
| Sessions and transcripts | ~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite | , |
| Legacy/archive session artifacts | ~/.openclaw/agents/<agentId>/sessions | , |
Single-agent mode (default)
With no configuration at all, OpenClaw operates a single agent:
agentIddefaults tomain.- Sessions key as
agent:main:<mainKey>(defaultmainKeyismain). - Workspace defaults to
<stateDir>/workspace(~/.openclaw/workspacefor the default install and~/.openclaw-<profile>/workspacefor a named profile). - State defaults to
~/.openclaw/agents/main/agent.
Agent helper
To introduce a new isolated agent:
openclaw agents add work
Flags: --workspace <dir>, --model <id>, --agent-dir <dir>, --bind <channel[:accountId]> (repeatable), --non-interactive (requires --workspace).
Add bindings to route inbound messages (the wizard offers to do this for you), then verify:
openclaw agents list --bindings
Agent provenance
OpenClaw tracks the origin of every configured agent: operator applies to CLI, onboarding, and Gateway requests; agent is used when the system agent made the request; claw covers agents added by a Claw install. Entries created by an agent also carry the requesting agent's id. Through its openclaw tool, a configured agent can ask OpenClaw to spin up another agent. The system agent logs the typed operation, surfaces the requesting agent id to the operator, and waits for operator approval before creating the agent. To view the current creation hierarchy, run:
openclaw agents list --tree
If a creator has been removed, its provenance still exists historically. When the creator is absent from the configured roster, its children show up at the root of the tree.
Quick start
Create each agent workspace
openclaw agents add coding
openclaw agents add social
Every agent is given its own workspace containing SOUL.md, AGENTS.md, and optionally USER.md, along with a dedicated agentDir and a session store located under ~/.openclaw/agents/<agentId>.
Create channel accounts
Set up one account per agent on the channels you use:
- Discord: create one bot per agent, turn on Message Content Intent, and copy each token.
- Telegram: use BotFather to create one bot per agent, then copy each token.
- WhatsApp: link a separate phone number for each account.
openclaw channels login --channel whatsapp --account work
Refer to the channel guides: Discord, Telegram, WhatsApp.
Add agents, accounts, and bindings
Agents go under agents.entries, channel accounts under channels.<channel>.accounts, and you connect them with bindings (examples follow).
Restart and verify
openclaw gateway restart
openclaw agents list --bindings
openclaw channels status --probe
Multiple agents, multiple personas
Each configured agentId acts as a separate persona boundary for core agent state:
- Distinct accounts per channel (per
accountId). - Distinct personalities (per-agent
AGENTS.md/SOUL.md). - Independent auth and sessions, with cross-agent access only possible through explicit features or plugin configuration.
This arrangement lets several people share a single Gateway while core agent state stays isolated.
Per-agent Memory Wiki vaults
By default, Memory Wiki relies on one global vault. To keep a support agent's compiled knowledge apart from a marketing agent's, assign plugins.entries.memory-wiki.config.vault.scope the value agent:
{
plugins: {
entries: {
"memory-wiki": {
enabled: true,
config: {
vault: {
scope: "agent",
path: "~/.openclaw/wiki",
},
},
},
},
},
}
The configured path serves as the parent directory. OpenClaw appends the normalized agent id, yielding paths like ~/.openclaw/wiki/support and ~/.openclaw/wiki/marketing. When multiple agents are configured, agent-scoped CLI and Gateway operations require an explicit agent. For bridge filtering, migration, and trust-boundary details, see Memory Wiki per-agent vaults.
Cross-agent memory search
The QMD cross-agent search path is gone. Builtin memory never searches another agent's transcript corpus; each agent only queries its own configured memory and eligible same-agent session sources. If the same reference material should be indexed by multiple agents, place intentionally shared Markdown in an explicit shared memory.search.extraPaths directory. The complete upgrade path is covered in Migrating from QMD.
One WhatsApp number, multiple people (DM split)
To route different WhatsApp DMs to different agents on one WhatsApp account, match the sender's E.164 (+15551234567) against peer.kind: "direct". Replies still originate from the same WhatsApp number, since there is no per-agent sender identity.
Note
Direct chats collapse to the agent's main session key by default, so true isolation requires one agent per person.
{
agents: {
entries: {
alex: { default: true, workspace: "~/.openclaw/workspace-alex" },
mia: { workspace: "~/.openclaw/workspace-mia" },
},
},
bindings: [
{
agentId: "alex",
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551230001" } },
},
{
agentId: "mia",
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551230002" } },
},
],
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551230001", "+15551230002"],
},
},
}
DM access control (pairing/allowlist) applies globally per WhatsApp account, not per agent. For shared groups, either bind the group to one agent or use Broadcast groups.
Routing rules
Bindings are deterministic, and the most specific one takes precedence. The full tier order (exact peer, parent peer, peer wildcard, guild+roles, guild, team, account, channel, default agent) is documented in Channel routing. A few rules deserve emphasis here:
- When several bindings match within the same tier, the first one in config order wins.
- If a binding specifies multiple match fields (for example
peer+guildId), every specified field must match (ANDsemantics). - A binding that leaves out
accountIdmatches only the default account, not all accounts. For a channel-wide fallback, useaccountId: "*"; for one account, useaccountId: "<name>". Re-adding the same binding with an explicit account id upgrades the existing channel-only binding rather than duplicating it.
For existing multi-agent configs, openclaw doctor --fix turns legacy ambient default routing into channel-wide bindings plus explicit heartbeat, Custodian, and Talk targets. Single-agent configs remain untouched.
Multiple accounts / phone numbers
Channels that support multiple accounts (e.g. WhatsApp) rely on accountId to identify each login. Each accountId routes to its own agent, so a single server can host multiple phone numbers without mixing sessions.
Set channels.<channel>.defaultAccount to pick the account that gets used whenever accountId is left out. If it's not configured, OpenClaw tries default when that's available, and otherwise goes with the first account id in sorted order.
These channels accept multiple accounts: discord, feishu, googlechat, imessage, irc, line, mattermost, matrix, nextcloud-talk, nostr, signal, slack, telegram, whatsapp, zalo, zalouser.
Concepts
agentId: a single "brain" that covers the workspace, per-agent authentication, and per-agent session storage.accountId: one instance of a channel account, such as WhatsApp accountpersonalversusbiz.binding: sends inbound messages to anagentIdbased on(channel, accountId, peer), and optionally on guild or team IDs.- Direct chats reduce to
agent:<agentId>:<mainKey>, which is the per-agent "main" channel; seesession.mainKey.
Platform examples
Discord bots per agent
A unique accountId is assigned to each Discord bot account. Attach each account to an agent, and maintain allowlists for every bot.
{
agents: {
entries: {
main: { default: true, workspace: "~/.openclaw/workspace-main" },
coding: { workspace: "~/.openclaw/workspace-coding" },
},
},
bindings: [
{ agentId: "main", match: { channel: "discord", accountId: "default" } },
{ agentId: "coding", match: { channel: "discord", accountId: "coding" } },
],
channels: {
discord: {
groupPolicy: "allowlist",
accounts: {
default: {
token: "DISCORD_BOT_TOKEN_MAIN",
guilds: {
"123456789012345678": {
channels: {
"222222222222222222": { enabled: true, requireMention: false },
},
},
},
},
coding: {
token: "DISCORD_BOT_TOKEN_CODING",
guilds: {
"123456789012345678": {
channels: {
"333333333333333333": { enabled: true, requireMention: false },
},
},
},
},
},
},
},
}
- Add each bot to the guild and turn on Message Content Intent.
- Tokens are stored in
channels.discord.accounts.<id>.token; the default account may rely onDISCORD_BOT_TOKEN.
Telegram bots per agent
{
agents: {
entries: {
main: { default: true, workspace: "~/.openclaw/workspace-main" },
alerts: { workspace: "~/.openclaw/workspace-alerts" },
},
},
bindings: [
{ agentId: "main", match: { channel: "telegram", accountId: "default" } },
{ agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } },
],
channels: {
telegram: {
accounts: {
default: {
botToken: "123456:ABC...",
dmPolicy: "pairing",
},
alerts: {
botToken: "987654:XYZ...",
dmPolicy: "allowlist",
allowFrom: ["tg:123456789"],
},
},
},
},
}
- Use BotFather to create a bot per agent, then copy each token.
- Tokens live in
channels.telegram.accounts.<id>.botToken; the default account can useTELEGRAM_BOT_TOKEN. - When running multiple bots inside one Telegram group, invite all of them and mention the specific bot that should respond.
- For each group bot, switch off BotFather Privacy Mode (
/setprivacy-> Disable), then remove and re-add the bot so Telegram picks up the change. - Allow groups with
channels.telegram.groups, or stick togroupPolicy: "open"for trusted group setups only. - Sender user IDs go into
groupAllowFrom. Group and supergroup IDs belong inchannels.telegram.groups, never ingroupAllowFrom. - Bind using
accountIdso every bot directs traffic to its own agent.
WhatsApp numbers per agent
Before the gateway starts, link each account:
openclaw channels login --channel whatsapp --account personal
openclaw channels login --channel whatsapp --account biz
~/.openclaw/openclaw.json (JSON5):
{
agents: {
entries: {
home: {
default: true,
name: "Home",
workspace: "~/.openclaw/workspace-home",
agentDir: "~/.openclaw/agents/home/agent",
},
work: {
name: "Work",
workspace: "~/.openclaw/workspace-work",
agentDir: "~/.openclaw/agents/work/agent",
},
},
},
// Deterministic routing: first match wins (most-specific first).
bindings: [
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
// Optional per-peer override (example: send a specific group to work agent).
{
agentId: "work",
match: {
channel: "whatsapp",
accountId: "personal",
peer: { kind: "group", id: "1203630...@g.us" },
},
},
],
// Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted.
tools: {
agentToAgent: {
enabled: false,
allow: ["home", "work"],
},
},
channels: {
whatsapp: {
accounts: {
personal: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/personal
// authDir: "~/.openclaw/credentials/whatsapp/personal",
},
biz: {
// Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
// authDir: "~/.openclaw/credentials/whatsapp/biz",
},
},
},
},
}
Common patterns
WhatsApp daily + Telegram deep work
Split by channel: point WhatsApp at a fast everyday agent and Telegram at an Opus agent.
{
agents: {
entries: {
chat: {
default: true,
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
opus: {
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
},
},
bindings: [
{ agentId: "chat", match: { channel: "whatsapp", accountId: "*" } },
{ agentId: "opus", match: { channel: "telegram", accountId: "*" } },
],
}
These examples rely on accountId: "*" so bindings stay valid if more accounts get added later. To send a single DM or group to Opus while everything else stays on chat, add a match.peer binding for that peer, since peer matches always take priority over channel-wide rules.
Same channel, one peer to Opus
Keep WhatsApp on the fast agent, but direct one DM to Opus:
{
agents: {
entries: {
chat: {
default: true,
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
opus: {
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
},
},
bindings: [
{
agentId: "opus",
match: { channel: "whatsapp", accountId: "*", peer: { kind: "direct", id: "+15551234567" } },
},
{ agentId: "chat", match: { channel: "whatsapp", accountId: "*" } },
],
}
Peer bindings always win, so place them above the channel-wide rule.
Family agent bound to a WhatsApp group
Attach a dedicated family agent to one WhatsApp group, using mention gating and a stricter tool policy:
{
agents: {
entries: {
family: {
default: true,
name: "Family",
workspace: "~/.openclaw/workspace-family",
identity: { name: "Family Bot" },
groupChat: {
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
},
sandbox: {
mode: "all",
scope: "agent",
},
tools: {
allow: [
"exec",
"read",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
},
},
},
},
bindings: [
{
agentId: "family",
match: {
channel: "whatsapp",
peer: { kind: "group", id: "120363999999999999@g.us" },
},
},
],
}
Tool allow and deny lists are tools, not skills. When a skill must run a binary, confirm exec is allowed and the binary exists in the sandbox. For tighter control, set agents.entries.*.groupChat.mentionPatterns and leave group allowlists enabled for the channel.
Per-agent sandbox and tool configuration
Each agent gets its own sandbox and tool restrictions:
{
agents: {
entries: {
personal: {
default: true,
workspace: "~/.openclaw/workspace-personal",
sandbox: {
mode: "off", // No sandbox for personal agent
},
// No tool restrictions - all tools available
},
family: {
workspace: "~/.openclaw/workspace-family",
sandbox: {
mode: "all", // Always sandboxed
scope: "agent", // One container per agent
docker: {
// Optional one-time setup after container creation
setupCommand: "apt-get update && apt-get install -y git curl",
},
},
tools: {
allow: ["read"], // Only read tool
deny: ["exec", "write", "edit", "apply_patch"], // Deny others
},
},
},
},
}
Note
setupCommandis located undersandbox.dockerand executes a single time when the container is created. If the resolved scope turns out to be"shared", any per-agentsandbox.docker.*overrides are disregarded.
This results in:
- Security isolation: limit which tools are available to agents you don't trust.
- Resource control: confine specific agents to a sandbox while others continue running on the host.
- Flexible policies: assign distinct permissions on an agent-by-agent basis.
Note
tools.elevatedis governed by both a global switch (tools.elevated.enabled/allowFrom) and an agent-level switch (agents.entries.*.tools.elevated.enabled/allowFrom). The agent-level switch can only tighten the global one; for elevated commands to execute, both must permit the sender. When targeting a group, rely onagents.entries.*.groupChat.mentionPatternsso that @mentions resolve to the correct agent.
For worked examples, refer to Multi-agent sandbox and tools.
Related
- ACP agents, operating external coding environments
- Channel routing, the path messages take to reach agents
- Presence, agent status and availability
- Session, session-level isolation and routing
- Sub-agents, launching background agent processes