docs/concepts/multi-agent.md
Run multiple isolated agents — each with its own workspace, state directory (agentDir), and session history — plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound messages are routed to the right agent through bindings.
An agent here is the full per-persona scope: workspace files, auth profiles, model registry, and session store. agentDir is the on-disk state directory that holds this per-agent config at ~/.openclaw/agents/<agentId>/. A binding maps a channel account (e.g. a Slack workspace or a WhatsApp number) to one of those agents.
An agent is a fully scoped brain with its own:
agentDir) for auth profiles, model registry, and per-agent config.~/.openclaw/agents/<agentId>/sessions.Auth profiles are per-agent. Each agent reads from its own:
~/.openclaw/agents/<agentId>/agent/auth-profiles.json
Skills are loaded from each agent workspace plus shared roots such as ~/.openclaw/skills, then filtered by the effective agent skill allowlist when configured. Use agents.defaults.skills for a shared baseline and agents.list[].skills for per-agent replacement. See Skills: per-agent vs shared and Skills: agent skill allowlists.
The Gateway can host one agent (default) or many agents side-by-side.
<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](/gateway/sandboxing). </Note>~/.openclaw/openclaw.json (or OPENCLAW_CONFIG_PATH)~/.openclaw (or OPENCLAW_STATE_DIR)~/.openclaw/workspace (or ~/.openclaw/workspace-<agentId>)~/.openclaw/agents/<agentId>/agent (or agents.list[].agentDir)~/.openclaw/agents/<agentId>/sessionsIf you do nothing, OpenClaw runs a single agent:
agentId defaults to main.agent:main:<mainKey>.~/.openclaw/workspace (or ~/.openclaw/workspace-<profile> when OPENCLAW_PROFILE is set).~/.openclaw/agents/main/agent.Use the agent wizard to add a new isolated agent:
openclaw agents add work
Then add bindings (or let the wizard do it) to route inbound messages.
Verify with:
openclaw agents list --bindings
```bash
openclaw agents add coding
openclaw agents add social
```
Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/<agentId>`.
- Discord: one bot per agent, enable Message Content Intent, copy each token.
- Telegram: one bot per agent via BotFather, copy each token.
- WhatsApp: link each phone number per account.
```bash
openclaw channels login --channel whatsapp --account work
```
See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp).
With multiple agents, each agentId becomes a fully isolated persona:
accountId).AGENTS.md and SOUL.md).This lets multiple people share one Gateway server while keeping their AI "brains" and data isolated.
If one agent should search another agent's QMD session transcripts, add extra collections under agents.list[].memorySearch.qmd.extraCollections. Use agents.defaults.memorySearch.qmd.extraCollections only when every agent should inherit the same shared transcript collections.
{
agents: {
defaults: {
workspace: "~/workspaces/main",
memorySearch: {
qmd: {
extraCollections: [{ path: "~/agents/family/sessions", name: "family-sessions" }],
},
},
},
list: [
{
id: "main",
workspace: "~/workspaces/main",
memorySearch: {
qmd: {
extraCollections: [{ path: "notes" }], // resolves inside workspace -> collection named "notes-main"
},
},
},
{ id: "family", workspace: "~/workspaces/family" },
],
},
memory: {
backend: "qmd",
qmd: { includeDefaultMemory: false },
},
}
The extra collection path can be shared across agents, but the collection name stays explicit when the path is outside the agent workspace. Paths inside the workspace remain agent-scoped so each agent keeps its own transcript search set.
You can route different WhatsApp DMs to different agents while staying on one WhatsApp account. Match on sender E.164 (like +15551234567) with peer.kind: "direct". Replies still come from the same WhatsApp number (no per-agent sender identity).
Example:
{
agents: {
list: [
{ id: "alex", workspace: "~/.openclaw/workspace-alex" },
{ id: "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"],
},
},
}
Notes:
Bindings are deterministic and most-specific wins:
<Steps> <Step title="peer match"> Exact DM/group/channel id. </Step> <Step title="parentPeer match"> Thread inheritance. </Step> <Step title="guildId + roles"> Discord role routing. </Step> <Step title="guildId"> Discord. </Step> <Step title="teamId"> Slack. </Step> <Step title="accountId match for a channel"> Per-account fallback. </Step> <Step title="Channel-level match"> `accountId: "*"`. </Step> <Step title="Default agent"> Fallback to `agents.list[].default`, else first list entry, default: `main`. </Step> </Steps> <AccordionGroup> <Accordion title="Tie-breaking and AND semantics"> - If multiple bindings match in the same tier, the first one in config order wins. - If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics). </Accordion> <Accordion title="Account-scope detail"> - A binding that omits `accountId` matches the default account only. - Use `accountId: "*"` for a channel-wide fallback across all accounts. - If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it. </Accordion> </AccordionGroup>Channels that support multiple accounts (e.g. WhatsApp) use accountId to identify each login. Each accountId can be routed to a different agent, so one server can host multiple phone numbers without mixing sessions.
If you want a channel-wide default account when accountId is omitted, set channels.<channel>.defaultAccount (optional). When unset, OpenClaw falls back to default if present, otherwise the first configured account id (sorted).
Common channels supporting this pattern include:
whatsapp, telegram, discord, slack, signal, imessageirc, line, googlechat, mattermost, matrix, nextcloud-talkbluebubbles, zalo, zalouser, nostr, feishuagentId: one "brain" (workspace, per-agent auth, per-agent session store).accountId: one channel account instance (e.g. WhatsApp account "personal" vs "biz").binding: routes inbound messages to an agentId by (channel, accountId, peer) and optionally guild/team ids.agent:<agentId>:<mainKey> (per-agent "main"; session.mainKey).```json5
{
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace-main" },
{ id: "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": { allow: true, requireMention: false },
},
},
},
},
coding: {
token: "DISCORD_BOT_TOKEN_CODING",
guilds: {
"123456789012345678": {
channels: {
"333333333333333333": { allow: true, requireMention: false },
},
},
},
},
},
},
},
}
```
- Invite each bot to the guild and enable Message Content Intent.
- Tokens live in `channels.discord.accounts.<id>.token` (default account can use `DISCORD_BOT_TOKEN`).
- Create one bot per agent with BotFather and copy each token.
- Tokens live in `channels.telegram.accounts.<id>.botToken` (default account can use `TELEGRAM_BOT_TOKEN`).
```bash
openclaw channels login --channel whatsapp --account personal
openclaw channels login --channel whatsapp --account biz
```
`~/.openclaw/openclaw.json` (JSON5):
```js
{
agents: {
list: [
{
id: "home",
default: true,
name: "Home",
workspace: "~/.openclaw/workspace-home",
agentDir: "~/.openclaw/agents/home/agent",
},
{
id: "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: "[email protected]" },
},
},
],
// 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",
},
},
},
},
}
```
```json5
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
bindings: [
{ agentId: "chat", match: { channel: "whatsapp" } },
{ agentId: "opus", match: { channel: "telegram" } },
],
}
```
Notes:
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
```json5
{
agents: {
list: [
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-6",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-6",
},
],
},
bindings: [
{
agentId: "opus",
match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } },
},
{ agentId: "chat", match: { channel: "whatsapp" } },
],
}
```
Peer bindings always win, so keep them above the channel-wide rule.
```json5
{
agents: {
list: [
{
id: "family",
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: "[email protected]" },
},
},
],
}
```
Notes:
- Tool allow/deny lists are **tools**, not skills. If a skill needs to run a binary, ensure `exec` is allowed and the binary exists in the sandbox.
- For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep group allowlists enabled for the channel.
Each agent can have its own sandbox and tool restrictions:
{
agents: {
list: [
{
id: "personal",
workspace: "~/.openclaw/workspace-personal",
sandbox: {
mode: "off", // No sandbox for personal agent
},
// No tool restrictions - all tools available
},
{
id: "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
},
},
],
},
}
Benefits:
See Multi-agent sandbox and tools for detailed examples.