Multi-agent routing: agent boundaries, channel accounts, and bindings

Learn how to run multiple isolated agents inside a single Gateway process. Each agent gets its own workspace, session history, and channel accounts, with bindings directing incoming messages to the correct agent.

Run multiple isolated agents inside a single Gateway process. Each agent gets its own workspace, a state directory (agentDir), SQLite-backed session history, and several channel accounts (for example, two different WhatsApp numbers). Incoming messages are directed to the correct agent using bindings.

An agent represents the complete per-persona environment: workspace files, authentication profiles, a model registry, and session storage. A binding links a channel account (such as a Slack workspace or a WhatsApp number) to a specific agent.

What is one agent

Every agent has its own:

  • Workspace: files, AGENTS.md/SOUL.md/USER.md, local notes, and persona rules.
  • State directory (agentDir): authentication profiles, model registry, and per-agent configuration.
  • Session store: chat history and routing state kept in ~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite.

Authentication profiles are scoped per agent and read from:

~/.openclaw/agents/<agentId>/agent/auth-profiles.json

Note

sessions_history is the safer way to retrieve cross-session context: it provides a bounded, redacted view instead of a full transcript dump. It removes thinking block signatures, tool result payload details, <relevant-memories> scaffolding, tool-call XML tags (<tool_call>, <function_call>, and their plural or downgraded forms), and MiniMax tool-call XML. It also truncates and caps the output by byte size.

Warning

Do not reuse agentDir across agents. Doing so causes authentication and session state conflicts. If a secondary agent's local OAuth credential expires or its refresh fails, OpenClaw falls back to the default or main agent's credential for the same profile ID and uses whichever token is newer, without copying the refresh token into the secondary agent's store. For a fully independent OAuth account, sign in from that agent directly. If you manually copy credentials, only copy portable static api_key or token profiles. OAuth refresh material is not portable by default, though copyToAgents can explicitly opt a profile in.

Skills are loaded from each agent's workspace and from shared roots like ~/.openclaw/skills, then filtered 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 should not share compiled wiki knowledge.

Note

Workspace note: each agent's workspace is the default current working directory, not a strict sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See Sandboxing.

Paths

WhatDefaultOverride
Config~/.openclaw/openclaw.jsonOPENCLAW_CONFIG_PATH
State dir~/.openclawOPENCLAW_STATE_DIR
Default agent's workspace~/.openclaw/workspace (or workspace-<profile> when OPENCLAW_PROFILE is set)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>/agentagents.entries.*.agentDir
Sessions and transcripts~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite,
Legacy/archive session artifacts~/.openclaw/agents/<agentId>/sessions,

Single-agent mode (default)

If you do not configure anything, OpenClaw runs a single agent:

  • agentId defaults to main.
  • Sessions are keyed as agent:main:<mainKey> (default mainKey is main).
  • The workspace defaults to ~/.openclaw/workspace (or workspace-<profile> when OPENCLAW_PROFILE is set to something other than default).
  • The state defaults to ~/.openclaw/agents/main/agent.

Agent helper

Add 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 direct incoming messages (the wizard can do this automatically), then confirm:

openclaw agents list --bindings

Quick start

Create each agent workspace

openclaw agents add coding
openclaw agents add social

Each agent receives its own workspace containing SOUL.md, AGENTS.md, and optionally USER.md, along with a dedicated agentDir and session store located under ~/.openclaw/agents/<agentId>.

Create channel accounts

Set up one account per agent on your chosen platforms:

  • Discord: deploy one bot per agent, turn on Message Content Intent, and save each token.
  • Telegram: create one bot per agent through BotFather and save each token.
  • WhatsApp: associate one phone number per account.
openclaw channels login --channel whatsapp --account work

Refer to platform-specific guides: Discord, Telegram, WhatsApp.

Add agents, accounts, and bindings

Register agents under agents.entries, channel accounts under channels.<channel>.accounts, and link them together with bindings (see examples below).

Restart and verify

openclaw gateway restart
openclaw agents list --bindings
openclaw channels status --probe

Multiple agents, multiple personas

Every configured agentId defines a separate persona boundary for core agent state:

  • Distinct accounts per channel (based on accountId).
  • Unique personalities (via per-agent AGENTS.md/SOUL.md).
  • Isolated authentication and sessions, with cross-agent access permitted only through explicit features or plugin configuration.

This setup allows multiple users to share a single Gateway while keeping each agent's core state isolated.

Per-agent Memory Wiki vaults

Memory Wiki uses one global vault by default. To separate a support agent's compiled knowledge 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 specified path acts as the parent directory. OpenClaw appends the normalized agent id, resulting in paths like ~/.openclaw/wiki/support and ~/.openclaw/wiki/marketing. When multiple agents are configured, agent-scoped CLI and Gateway operations require an explicit agent identifier. Refer to Memory Wiki per-agent vaults for details on bridge filtering, migration, and trust boundaries.

To allow one agent to search another agent's QMD session transcripts, add extra collections under agents.entries.*.memory.search.qmd.extraCollections. Use memory.search.qmd.extraCollections when all agents should access the same collections.

{
  agents: {
    defaults: {
      workspace: "~/workspaces/main",
    },
    entries: {
      main: {
        workspace: "~/workspaces/main",
        memory: {
          search: {
            qmd: {
              extraCollections: [{ path: "notes" }], // resolves inside workspace -> collection named "notes-main"
            },
          },
        },
      },
      family: { workspace: "~/workspaces/family" },
    },
  },
  memory: {
    backend: "qmd",
    search: {
      qmd: {
        extraCollections: [{ path: "~/agents/family/sessions", name: "family-sessions" }],
      },
    },
    qmd: { includeDefaultMemory: false },
  },
}

An extra-collection path can be shared among agents, but its name remains explicit when the path falls outside the agent workspace. Paths inside the workspace stay agent-scoped so each agent maintains its own transcript search set.

One WhatsApp number, multiple people (DM split)

Direct different WhatsApp DMs to different agents on a single WhatsApp account by matching the sender's E.164 number (+15551234567) with peer.kind: "direct". Replies still originate from the same WhatsApp number, there is no per-agent sender identity.

Note

Direct conversations collapse to the agent's primary session key by default, so true isolation requires one agent per person.

{
  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"],
    },
  },
}

DM access control (pairing and allowlist) applies globally per WhatsApp account, not per agent. For shared groups, assign the group to one agent or use Broadcast groups.

Routing rules

Bindings follow deterministic rules where the most specific match wins. Consult Channel routing for the complete tier hierarchy (exact peer, parent peer, peer wildcard, guild+roles, guild, team, account, channel, default agent). Several notable rules:

  • When multiple bindings match within the same tier, the first one in configuration order takes precedence.
  • If a binding specifies multiple match fields (for instance peer + guildId), every listed field must match (AND semantics).
  • A binding that omits accountId matches only the default account, not all accounts. Use accountId: "*" for a channel-wide fallback, or accountId: "<name>" for a single account. Adding the same binding again with an explicit account id upgrades the existing channel-only binding rather than duplicating it.

Multiple accounts / phone numbers

Channels that support multiple accounts (such as WhatsApp) use accountId to distinguish 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 specify the account used when accountId is not provided. When unset, OpenClaw falls back to default if available, otherwise the first configured account id (sorted).

Channels that support multiple accounts: discord, feishu, googlechat, imessage, irc, line, mattermost, matrix, nextcloud-talk, nostr, signal, slack, telegram, whatsapp, zalo, zalouser.

Concepts

  • agentId: a single "brain" (workspace, per-agent authentication, per-agent session store).
  • accountId: one channel account instance (for example, a WhatsApp account personal compared to biz).
  • binding: directs incoming messages to an agentId based on (channel, accountId, peer), and optionally guild or team IDs.
  • Direct conversations reduce to agent:<agentId>:<mainKey> (per-agent "main"; refer to session.mainKey).

Platform examples

Discord bots per agent

Each Discord bot account is tied to a unique accountId. Link every account to an agent and maintain allowlists for each bot.

{
  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 },
              },
            },
          },
        },
      },
    },
  },
}
  • Add each bot to the guild and activate Message Content Intent.
  • Tokens are stored in channels.discord.accounts.<id>.token (the default account may use DISCORD_BOT_TOKEN).

Telegram bots per agent

{
  agents: {
    list: [
      { id: "main", workspace: "~/.openclaw/workspace-main" },
      { id: "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 one bot per agent and copy each token.
  • Tokens reside in channels.telegram.accounts.<id>.botToken (the default account can rely on TELEGRAM_BOT_TOKEN).
  • When running multiple bots in the same Telegram group, invite each bot and mention the one that should reply.
  • Turn off BotFather Privacy Mode for each group bot (/setprivacy -> Disable), then remove and re-add the bot so Telegram applies the change.
  • Authorize groups with channels.telegram.groups, or restrict groupPolicy: "open" to trusted group setups only.
  • Place sender user IDs in groupAllowFrom. Group and supergroup IDs go into channels.telegram.groups, not groupAllowFrom.
  • Bind using accountId so every bot directs traffic to its own agent.

WhatsApp numbers per agent

Before starting the gateway, link each account:

openclaw channels login --channel whatsapp --account personal
openclaw channels login --channel whatsapp --account biz

~/.openclaw/openclaw.json (JSON5):

{
  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: "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

Separate by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent.

{
  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", accountId: "*" } },
    { agentId: "opus", match: { channel: "telegram", accountId: "*" } },
  ],
}

These examples use accountId: "*" so bindings stay valid if you add accounts later. To send a single DM or group to Opus while leaving the rest on chat, add a match.peer binding for that peer. 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: {
    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", 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 a single WhatsApp group, with mention gating and a stricter tool policy:

{
  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: "120363999999999999@g.us" },
      },
    },
  ],
}

Tool allow and deny lists refer to tools, not skills. If a skill must run a binary, make sure exec is permitted and the binary exists inside the sandbox. For tighter control, set agents.entries.*.groupChat.mentionPatterns and keep group allowlists enabled for the channel.

Per-agent sandbox and tool configuration

Every 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
        },
      },
    ],
  },
}

Note

setupCommand is located inside sandbox.docker and executes a single time during container creation. When the resolved scope equals "shared", per-agent sandbox.docker.* overrides are disregarded.

This provides the following advantages:

  • Security isolation: limit which tools an untrusted agent can access.
  • Resource control: place certain agents in a sandbox while leaving others on the host.
  • Flexible policies: assign distinct permissions to each agent.

Note

tools.elevated includes both a global gate (tools.elevated.enabled/allowFrom) and a per-agent gate (agents.entries.*.tools.elevated.enabled/allowFrom). The per-agent gate can only tighten the global gate's restrictions. Both must permit a sender before elevated commands can execute. For group targeting, use agents.entries.*.groupChat.mentionPatterns so @mentions correctly resolve to the target agent.

Detailed examples are available in Multi-agent sandbox and tools.