OpenClaw is an open-source AI agent platform. You can create multiple agents, connect to multiple messaging systems, connect different tools, MCPs etc… It comes with a lot of built-in plugins and tools that lets agents do the stuff.
OpenClaw has multiple components:

In this post, we will learn about high-level architecture of Openclaw’s Gateway hub-and-spoke architecture, how the device binding handshake workflow works, setting up Openclaw, leveraging Openclaw for different usecases, creating an agentic operating system using OpenClaw agent runtime to manage mutliple agents, context and sessions.
You can set up OpenClaw to monitor your inbox and alert you on important emails. The agent parses incoming emails, filters based on rules you define, and sends notifications to your preferred messaging channel.



OpenClaw can monitor your financial email alerts (credit card transactions, bank notifications) and track your spending automatically. Set up a cron that parses transaction emails and logs them to a spreadsheet or database.


Set up OpenClaw to fetch weather reports on a schedule and send you daily summaries or alerts for specific conditions (rain, extreme temperatures, etc.).

The Gateway is the hub. It is one long-lived process. It owns every messaging surface: WhatsApp, Telegram, Slack, and Discord. It also exposes a typed WebSocket API on 127.0.0.1:18789.
Three kinds of spokes connect to the Gateway:
role: node and lists its own capabilities and commands.The Gateway stores all state in one SQLite database at ~/.openclaw/state/openclaw.sqlite. This database holds paired devices, approved node capabilities, and pending requests.
Every request and event follows one shape. A client sends a request: {type: "req", id, method, params}. The Gateway sends back a response, or it emits an event: {type: "event", event, payload}. This one shape makes the protocol simple to parse and simple to extend.
The Gateway WebSocket API follows a typed protocol i.e. every method, every request, and every event has a schema. The Gateway checks each message against that schema before it acts on the message.
The three frame shapes. A client and the Gateway exchange exactly three kinds of frames:
{type: "req", id, method, params, traceparent?}. A client sends this to call a method.{type: "res", id, ok, payload|error}. The Gateway sends this back. The id field matches the request’s id, so a client can match each response to the request that caused it.{type: "event", event, payload, seq?, stateVersion?}. The Gateway sends this on its own, without a matching request. seq and stateVersion let a client detect a missed or out-of-order event.How an error looks : When ok is false, the error field has this shape: {code, message, details?, retryable?, retryAfterMs?}. A client should read code (and details.code when present) to decide what to do next. The message field is only a human-readable fallback — it can change between versions, so code should not match against it. A common error is a missing scope: {code: "FORBIDDEN", details: {code: "MISSING_SCOPE", missingScope, requiredScopes}}.
Sources: Gateway architecture
Openclaw opens HTTP / Websocket connections with the messaging providers depending on the type of integration. In case of Slack, two WebSocket connections would be opened with Slack, one from Gateway and another from user’s client apps. This facilitates full duplex communication between the three parties.
The user and the Gateway never talk to each other directly. Slack sits in the middle of both connections, relaying between them.
A client cannot talk to the Gateway the first time it connects. It must pass two checks in order:
Identity setup, done once per client:
deviceId = SHA-256(publicKey).hex. The ID is not assigned by the Gateway — it falls out of the key itself.The handshake, done on every connect:
{"type": "event", "event": "connect.challenge", "nonce": "...", "ts": <timestamp>}. The nonce is a one-time value; the client cannot predict it ahead of time.v1|deviceId|clientId|clientMode|role|scopes|signedAtMs|token. A remote client appends the Gateway’s nonce to this payload. This stops replay attacks such that the previously valid signed request from being replayed later, since the nonce changes on every challenge. The nonce is not required to be appended on loopback clients.{"type": "req", "method": "connect", "params": {"device": {"id", "publicKey", "signature", "signedAt", "nonce"}}}. The signature key contains the signed payload. The client also sends the individual field values, id, publicKey, signature, signedAt, nonce, plus clientId, clientMode, role, scopes, and token in the request so that the gateway can reconstruct the string and verify the signature.publicKey, and checks that signedAt is within DEVICE_SIGNATURE_SKEW_MS (10 minutes) of its own clock.{"type": "res", "ok": true, "payload": {"type": "hello-ok", "auth": {"role", "scopes", "deviceToken"}}}.This step doesn’t authorise the client to talk to gateway yet. This only establishes the handshaking of node/device with the gateway. The approval process is at the gateway and the admin/operator has to approve from commandline.
Once identity is proven, a client still is not trusted by default. The pairing to be manually approved by the admin via CLI. The Gateway applies one of four rules, in this order:
gateway.nodes.pairing.autoApproveLocal: false.autoApproveCidrs. The Gateway approves it, but only when the node requests no extra scopes.openclaw node identity --json there. If the returned device ID and public key match the pending request, the Gateway approves the pairing. This rule applies only to direct private, ULA, link-local, or CGNAT addresses — never to a proxied or loopback address.node.pair.requested. An operator, using the CLI or a UI, must approve or reject it. This pending request expires after five minutes if no one acts on it.When the Gateway approves or rejects a pending request, it emits node.pair.resolved.
The Gateway is the single source of truth for pairing state. The macOS app and any control UI are only front ends: they show pending requests and send the approve or reject decision back to the Gateway.
The Gateway does not talk to a LLM model directly. It hands off the message to the Agent Runtime, an embedded process with its own agent loop, tools integration, and prompt assembly. Every agent the runtime manages is a self-contained unit: a workspace directory, a set of bootstrap files, a model choice, and its own session store. Nothing about one agent leaks into another unless the config says so.
Each agent gets exactly one workspace directory, and that directory is its cwd. On the first turn, OpenClaw reads a fixed set of files out of that workspace and folds them into the system prompt:
| File | Purpose | How the runtime uses it |
|---|---|---|
AGENTS.md |
Operating instructions and memory | Read once on the first turn of a session and folded into the system prompt’s Project Context; guides how the agent uses its tools; actual tool access is controlled separately by tools.allow/tools.deny |
SOUL.md |
Persona, boundaries, tone | Read once on the first turn; defines the character’s boundaries and tone in Project Context |
IDENTITY.md |
Agent name, vibe, emoji | Read once on the first turn; supplies the name/vibe/emoji half of the persona alongside SOUL.md |
USER.md |
User profile and preferred form of address | Read once on the first turn; personalizes replies to whoever is on the other end |
BOOTSTRAP.md |
One-time initialization ritual | Only created for a new workspace; while present, the runtime adds extra system-prompt guidance for the ritual; the runtime never deletes it for you — remove it yourself once the ritual is done, and it won’t come back on later restarts |
MEMORY.md |
Long-term memory (optional) | Not part of the fixed set — only injected when it exists at the workspace root; the runtime never writes back to it, so anything durable has to be saved there manually |
These files are what keeps each agent distinct from each other. To be honest, we don’t require these many files, it complicates the system. Most of these files are read at the beginning of the session, so editing SOUL.md mid-conversation has no effect until the next session starts.
The minimum viable config only needs a workspace:
{
agents: {
defaults: {
workspace: "<path>"
}
}
}To run more than one agent, define named entries under agents.entries instead of relying on defaults. Each entry can override the workspace, the state directory, and the model:
{
agents: {
entries: {
home: {
default: true,
workspace: "~/.openclaw/workspace-home",
model: "anthropic/claude-sonnet-4-6"
},
work: {
workspace: "~/.openclaw/workspace-work",
model: "anthropic/claude-opus-4-6"
}
}
}
}workspace holds the bootstrap files above. agentDir (defaults to ~/.openclaw/agents/<agentId>/agent) holds the per-agent SQLite session store and auth profiles, so home and work never share session state even though they run inside the same Gateway process.
With multiple agents defined, something has to decide which agent answers a given message. That’s what bindings are for — they map an inbound message to an agentId using a most-specific-match hierarchy: exact peer, then guild/role, then account, then channel-wide, then the agent marked default.
{
bindings: [
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } }
],
channels: {
whatsapp: {
accounts: {
personal: {},
biz: {}
}
}
}
}Here, a WhatsApp message from the biz account always lands with the work agent, and one from personal lands with home — two personas, two model choices, two session stores, sharing one Gateway and one WhatsApp connection.
Because each agent is isolated down to its own tool policy, you can run a trusted agent next to a locked-down one in the same config:
{
agents: {
entries: {
trusted: {
workspace: "~/.openclaw/workspace-trusted",
tools: { allow: ["read", "write", "exec"] }
},
restricted: {
workspace: "~/.openclaw/workspace-restricted",
sandbox: { mode: "all", scope: "agent" },
tools: { deny: ["write", "exec"] }
}
}
}
}tools.allow / tools.deny scope which built-in tools an agent can call, and sandbox.mode / sandbox.scope control whether that agent’s tool calls run sandboxed. Cross-agent access is off by default too — tools.agentToAgent has to explicitly allow a pair of agents to talk to each other, otherwise restricted has no path into trusted’s session or workspace.
Sources: Agent runtime, Multi-agent routing
OpenClaw’s Agent Runtime can be paired with existing coding agents and development tools. You might already use tools like OpenAI Codex CLI, Claude Code CLI, or Cursor directly in your terminal or IDE. OpenClaw doesn’t replace these. It becomes the session and routing layer that lets you run all of them from one place, with one source of truth for context.
OpenClaw supports two integration patterns for coding harnesses:
WebSocket/ACP integration. The coding harness connects to OpenClaw’s Gateway over WebSocket, authenticates via the Ed25519 handshake, and sends requests to OpenClaw. OpenClaw routes to the configured agent. This is how OpenCode integrates.
Tool-based integration. OpenClaw calls the coding harness’s CLI as a tool. You configure OpenClaw with an exec tool that runs codex or claude commands. OpenClaw shells out to the CLI, passes context, and returns the response. This is how you’d integrate Codex CLI and Claude Code CLI, since they don’t provide API keys - you use them via their CLIs.
Which pattern you use depends on what the harness supports. OpenCode has native WebSocket support. Codex CLI and Claude Code CLI are invoked as shell commands.
| Harness | Integration Method | Built-in OpenClaw Support | What You Gain |
|---|---|---|---|
| OpenCode CLI | WebSocket. OpenCode connects to the Gateway, authenticates via Ed25519 handshake, and routes requests through OpenClaw. | Yes. Native integration. Configure ~/.config/opencode/opencode.json with Gateway URL and device credentials. |
Unified session state across terminal, Slack, Telegram. Multi-agent routing. Tool policies per workspace. |
| OpenAI Codex CLI / Claude Code CLI | Tool/CLI. OpenClaw runs codex or claude as an exec tool. Pass prompts, get responses. No API key needed - subscription auth is handled by the CLI. |
No. Requires exec tool config to shell out to the CLI. |
Use Codex or Claude’s code generation through OpenClaw’s routing, with subscription auth handled by their CLIs. |
| Cursor IDE | Tool/CLI. OpenClaw can invoke Cursor’s agent capabilities via its CLI (cursor command) as an exec tool, similar to Codex and Claude Code. Subscription auth is handled by Cursor. |
No. Requires exec tool config to shell out to Cursor CLI. |
Use Cursor’s code generation from OpenClaw, with subscription auth handled by Cursor. |
If you use OpenCode as your primary coding assistant, you can route it through OpenClaw like this:
{
agents: {
entries: {
coding: {
workspace: "~/.openclaw/workspace-coding",
model: "anthropic/claude-sonnet-4-6",
tools: { allow: ["read", "write", "exec"] }
}
}
}
}~/.config/opencode/opencode.json:{
provider: "openclaw",
endpoint: "ws://127.0.0.1:18789",
agentId: "coding"
}coding agent. The agent’s session state stays in ~/.openclaw/agents/coding/agent.The same agent can also answer messages from Slack or Telegram if you bind those surfaces to the coding agent. One agent, multiple surfaces, consistent context.
If you have a Codex or Claude Code subscription and use their CLI, you can still route through OpenClaw by calling the CLI as a tool:
# For Claude Code
npm install -g @anthropic-ai/claude-code
claude auth login
# For Codex
npm install -g @openai/codex
codex auth loginexec tool that calls the CLI:{
agents: {
entries: {
coding: {
workspace: "~/.openclaw/workspace-coding",
model: "anthropic/claude-sonnet-4-6", // or your preferred model
tools: {
allow: ["read", "write", "exec"]
},
exec: {
commands: {
codex: "codex",
claude: "claude"
}
}
}
}
}
}AGENTS.md for when to use each tool:## Coding Tools
- Use the `claude` command for code generation and refactoring.
- Use the `codex` command for OpenAI-specific tasks.
- Pass the user's request as the prompt to the CLI.Now when you ask OpenClaw to write code, it can shell out to Codex or Claude Code CLI, and you get subscription-based auth plus OpenClaw’s session storage and multi-channel routing.
OpenClaw is designed to be more than an assistant you talk to. It’s a foundation for building your own Agent Operating System: a custom application with specialized agents for marketing, sales, customer support, internal tools, or any domain where you want multiple agents, each with its own persona, tools, and context.
A custom Agent OS built on OpenClaw looks like this:
Frontend (your custom UI). A web or mobile app with a chat interface, dashboards, and domain-specific controls. The UI talks to your backend server, not directly to OpenClaw.
Backend server (your app server). Your server handles user authentication, session management, and business logic. It connects to the OpenClaw Gateway over the WebSocket API and authenticates via the same cryptographic handshake any control-plane client uses.
OpenClaw Gateway. The Gateway handles all agent runtime concerns: model calls, tool execution, session storage, and multi-agent routing. Your backend hands off messages to the Gateway and receives responses.
Specialized agents. Each agent has its own workspace, bootstrap files, model choice, and tool policy. You define these in the OpenClaw config, and your backend routes user requests to the right agent based on your application’s logic.
Your backend server authenticates with the OpenClaw Gateway using the same Ed25519 handshake described earlier. Here’s how it fits together:
ws://<gateway-host>:18789.req frame to the Gateway, specifying which agent should handle the request.res frame with the response.This layered approach keeps user auth in your backend, and agent auth between your backend and the Gateway. The Gateway never sees your users’ credentials.
Say you want to build an internal tool with three agents:
| Agent | Purpose | Tools | Model |
|---|---|---|---|
| Marketing | Drafts blog posts, social media copy, and campaign emails | read, write, web_search |
anthropic/claude-sonnet-4-6 |
| Sales | Answers prospect questions, drafts proposals, logs CRM updates | read, write, exec (for CRM CLI) |
anthropic/claude-sonnet-4-6 |
| Support | Answers customer questions, looks up orders, creates tickets | read, exec (for ticketing API) |
anthropic/claude-sonnet-4-6 |
Your OpenClaw config might look like:
{
agents: {
entries: {
marketing: {
workspace: "/opt/agent-os/workspaces/marketing",
model: "anthropic/claude-sonnet-4-6",
tools: { allow: ["read", "write", "web_search"] }
},
sales: {
workspace: "/opt/agent-os/workspaces/sales",
model: "anthropic/claude-sonnet-4-6",
tools: { allow: ["read", "write", "exec"] }
},
support: {
workspace: "/opt/agent-os/workspaces/support",
model: "anthropic/claude-sonnet-4-6",
tools: { allow: ["read", "exec"] }
}
}
}
}Your backend routes incoming messages based on the user’s selection in the UI. If the user selects “Marketing” from a dropdown, your backend sends the message to the Gateway with agentId: "marketing".
Your backend server can run:
OpenClaw’s Gateway is a single process. If you need to scale:
Sources: Gateway security, Multi-agent routing
A lot of them. Telegram, Slack, Discord, WhatsApp, Signal, iMessage, Microsoft Teams, Google Chat, Matrix, Mattermost, IRC, LINE, Nextcloud Talk, Nostr, QQ Bot, Raft, SMS, Synology Chat, Tlon, Twitch, Voice Call, Feishu, Zalo, and Zalo Personal, and there are external plugins for a few more like WeChat and Yuanbao. Group chats work with mention-based activation, and DMs are protected by the allowlists.
Openclaw doesn’t have its own model — it’s a runtime that calls whichever model you point it at. For a hosted provider you bring your own API key, or authenticate via subscription OAuth where that’s supported. If you’d rather not send anything to a hosted provider at all, you can point Openclaw at a local model instead, and no API key is needed.
The usual hosted providers — Anthropic, OpenAI, Google, Opencode and others — plus self-hosted/local runtimes: vLLM, SGLang, Ollama, llama.cpp, and LM Studio that lets you run an agent entirely on your own hardware, as mentioned in the use cases above.
Any model your configured provider exposes. A model reference in config is just provider/model (for example anthropic/claude-sonnet-4-6 or openai/gpt-4o), and you need to provide the provider’s API key, add the model name. That’s all! Additionally, openclaw supports fallback chain i.e. if a model fails or rate limited, it fallback to another model. Refer Configuring model fallbacks section.
Each agent gets its own workspace directory. Inside that workspace, you place bootstrap files like AGENTS.md, SOUL.md, IDENTITY.md, and USER.md. These files define the agent’s behavior, persona, and context. Define multiple agents in your config under agents.entries, each pointing to its own workspace with its own set of bootstrap files. See the Agent Runtime section for details on workspace structure and the Multi-Agent Business OS example for a concrete config.
Yes. Each agent entry in the config can specify its own model field. For example, you could have a coding agent running on anthropic/claude-sonnet-4-6 for reasoning-heavy tasks, and a quick-response agent running on openai/gpt-4o-mini for simple queries. Each agent also gets its own session store, so conversations stay isolated.
Create a workspace with instructions in AGENTS.md that describe the task. For email monitoring, you might write: “You monitor the user’s inbox. When new emails arrive, summarize them and flag anything urgent. Respond in bullet points.” Then create a cron job in OpenClaw that triggers this agent on a schedule. The agent reads the email content and follows the instructions you wrote. See the Email Monitoring with Crons example above.
Each agent has its own workspace directory, its own session database in ~/.openclaw/agents/<agentId>/agent, and its own tool policy defined by tools.allow and tools.deny. By default, agents cannot access each other’s sessions or files. If you want two agents to communicate, you have to explicitly enable tools.agentToAgent in the config.
Not by default. Each agent maintains its own session and memory. If you need shared context, you have two options: (1) store shared state in a file both agents can read via the read tool, or (2) enable cross-agent communication via tools.agentToAgent and have one agent call the other as a tool.