Your personal AI gateway โ one brain, every chat app
Imagine you have a brilliant assistant that can read files, search the web, manage your calendar, and write code. Now imagine it can talk to you through any messaging app โ WhatsApp, Telegram, Discord, Feishu, even iMessage.
That's OpenClaw. It's an open-source gateway that connects your favorite chat apps to powerful LLMs like Claude, GPT, and Gemini. One configuration, all your channels.
Let's trace what happens when you send "What's the weather today?" from WhatsApp:
Your message hits the WhatsApp channel plugin
The gateway figures out which session you belong to and routes the message
The message processing pipeline checks for commands, builds context, and calls the LLM
The LLM's reply flows back through the pipeline, gets formatted, and lands in your WhatsApp chat
Every journey starts at src/entry.ts. When you run openclaw in your terminal, this is the first file that executes. Here's what kicks things off:
if (
!isMainModule({
currentFile: fileURLToPath(import.meta.url),
wrapperEntryPairs: [...ENTRY_WRAPPER_PAIRS],
})
) {
// Imported as a dependency โ skip
} else {
process.title = "openclaw";
ensureOpenClawExecMarkerOnProcess();
installProcessWarningFilter();
normalizeEnv();
}
First, check: "Am I being run directly by the user, or was I loaded as a library by someone else?"
If loaded as a library, do nothing โ we don't want duplicate gateways crashing into each other.
If we're the main show: label the process "openclaw" so it's easy to find in task managers.
Set up environment markers, filter noisy Node.js warnings, and normalize environment variables so everything is predictable.
The isMainModule check prevents a classic problem: if the bundler imports entry.ts as a dependency while index.ts is the actual entry point, the startup code would run twice โ launching two gateways that fight over the same port. This guard pattern is common in any Node.js app that can be both a library and a standalone program.
The five core subsystems that make OpenClaw tick
OpenClaw's source code is organized into distinct subsystems, each with a clear responsibility. Think of them as team members in a company โ each has a job title and doesn't step on anyone else's toes.
The boss โ boots everything up, manages auth, serves HTTP, handles WebSocket connections
The translators โ each plugin speaks a different messaging platform's language
The brain coordinator โ processes inbound messages, detects commands, calls the LLM
The memory keeper โ tracks conversation state, manages session lifecycle
The rule book โ YAML-based settings with hot-reload, schema validation, and migration
Let's let the components speak for themselves:
The Gateway is the most complex subsystem. When it starts, it orchestrates a careful dance:
const BOOT_FILENAME = "BOOT.md";
function buildBootPrompt(content: string) {
return [
"You are running a boot check.",
"Follow BOOT.md instructions exactly.",
"",
"BOOT.md:",
content,
].join("\n");
}
When the gateway boots, it looks for a file called BOOT.md in your workspace.
If found, it builds a special prompt that tells the AI: "Read these boot instructions and follow them."
This lets you automate startup tasks โ like sending a "Good morning!" message or checking your calendar when the server starts.
Follow a message from your phone to the AI and back
Click "Next Step" to trace a message through every component. Watch which subsystem lights up at each stage.
When a message arrives, the routing system makes a crucial decision. It examines the sender, channel, group, and configuration to determine which agent and session should handle the message:
export type ResolveAgentRouteInput = {
cfg: OpenClawConfig;
channel: string;
accountId?: string | null;
peer?: RoutePeer | null;
parentPeer?: RoutePeer | null;
guildId?: string | null;
memberRoleIds?: string[];
};
To figure out where a message goes, the router needs several pieces of context:
cfg โ the full configuration (rules, bindings, agent definitions)
channel โ which platform? (telegram, whatsapp, discordโฆ)
peer โ who's talking? (a DM contact, a group chat)
guildId / memberRoleIds โ Discord-specific: which server and what roles does the user have? This enables role-based agent routing.
After routing, the dispatchInboundMessage function takes over. It's the central coordinator that ties everything together:
export async function dispatchInboundMessage(params: {
ctx: MsgContext;
cfg: OpenClawConfig;
dispatcher: ReplyDispatcher;
}) {
const finalized = finalizeInboundContext(params.ctx);
return await withReplyDispatcher({
dispatcher: params.dispatcher,
run: () =>
dispatchReplyFromConfig({
ctx: finalized,
cfg: params.cfg,
dispatcher: params.dispatcher,
}),
});
}
This function is the handoff point. It receives the message context (who sent it, from where, what they said) and the full config.
First, "finalize" the context โ fill in any missing details, normalize the data.
Then, wrap everything in a reply dispatcher that manages typing indicators, buffering, and cleanup. The dispatcher ensures replies go out in order and resources are always released โ even if something crashes.
Inside, dispatchReplyFromConfig does the real work: it calls the AI, streams the response, and sends it back to the user.
Notice how the code wraps the AI call inside withReplyDispatcher? This is the resource management pattern โ the dispatcher is "reserved" before the work starts and "released" in a finally block, guaranteeing cleanup even if the AI call fails. This is the same principle behind database connection pools, file handles, and mutexes. It ensures typing indicators stop and channels aren't left hanging.
How one codebase speaks every messaging language
OpenClaw ships with plugins for nine messaging platforms, defined in a single array:
export const CHAT_CHANNEL_ORDER = [
"telegram",
"whatsapp",
"discord",
"irc",
"googlechat",
"slack",
"signal",
"imessage",
"line",
] as const;
This is the official list of built-in chat channels. The order matters โ it defines priority when the system needs to pick a default.
The as const makes TypeScript treat these as exact literal strings, not just "any string." This means the compiler catches typos like "telegarm" at build time.
Beyond these built-in channels, external plugins (like Feishu, Microsoft Teams) can register themselves at runtime through the plugin system.
Each channel plugin implements a set of adapters that define how it interacts with the rest of the system:
Receiving and sending messages โ the core of any channel plugin
Login, QR codes, token refresh โ each platform authenticates differently
Thread/topic support โ Discord threads, Telegram topics, Slack threads
Connection health, error reporting, and diagnostics
Real-time message streaming โ edit-in-place as the AI types
Group chat behavior โ mentions, join/leave, group-specific settings
The plugin registry uses a pattern where bundled plugins are auto-discovered from generated code, and external plugins can register at runtime:
export function normalizeAnyChannelId(
raw?: string | null
): ChannelId | null {
const key = normalizeChannelKey(raw);
if (!key) return null;
return findRegisteredChannelPluginEntry(key)
?.plugin.id ?? null;
}
When the system encounters a channel name (from config, a message, etc.), it normalizes it โ trims whitespace, lowercases it.
Then it searches all registered plugins (both built-in and external) by ID and aliases. "wa" might match "whatsapp", for example.
If no match is found, it returns null โ the message came from an unknown channel and can't be processed.
OpenClaw's channel system is a textbook example of the Strategy pattern: define an interface (the adapters), then let each implementation (Telegram, WhatsApp, etc.) handle the details differently. The core system never needs to know how WhatsApp sends a message โ it just calls send() on whatever plugin is active. This makes adding new platforms a matter of implementing the adapters, not rewriting the core.
How OpenClaw extends its capabilities and spawns workers
Skills are OpenClaw's extensibility layer. They're simple Markdown files (SKILL.md) that get injected into the AI's system prompt. Each skill teaches the agent how to do something new.
feishu-bitable
Create and manage Feishu spreadsheet-like databases
github
Issues, PRs, CI runs, code review via gh CLI
coding-agent
Delegate coding tasks to Codex, Claude Code, or Pi agents
weather
Get forecasts via wttr.in or Open-Meteo APIs
The system prompt builder scans for skills and presents them to the AI as a menu of capabilities:
function buildSkillsSection(params: {
skillsPrompt?: string;
readToolName: string;
}) {
return [
"## Skills (mandatory)",
"Before replying: scan <available_skills>",
`- If exactly one skill applies: read its SKILL.md with \`${params.readToolName}\`, then follow it.`,
"- If multiple could apply: choose the most specific one.",
"- If none apply: do not read any SKILL.md.",
params.skillsPrompt,
];
}
This builds the "Skills" section that goes into the AI's system prompt. It's like handing someone a toolbox catalog.
The instructions tell the AI: "Before you respond to the user, check if any skill matches their request."
If a match is found, the AI reads the full SKILL.md file using its file-reading tool โ loading detailed instructions on-demand rather than cramming everything into the prompt upfront.
This is a lazy-loading pattern: skills are listed as summaries, and full instructions are only fetched when needed.
Sometimes a single AI brain isn't enough. OpenClaw can spawn independent sub-agents for parallel work:
export type SpawnSubagentParams = {
task: string;
label?: string;
agentId?: string;
model?: string;
thinking?: string;
runTimeoutSeconds?: number;
mode?: SpawnSubagentMode;
attachments?: Array<{
name: string;
content: string;
encoding?: "utf8" | "base64";
}>;
};
When spawning a sub-agent, you configure it with:
task โ what should this worker do? (natural language description)
model โ which AI brain? (Claude, GPT, Geminiโฆ)
thinking โ how deeply should it reason? (low/medium/high)
runTimeoutSeconds โ kill it if it takes too long
attachments โ files to give the sub-agent to work with
It's like hiring a contractor: here's the job, the budget (timeout), and the materials (attachments).
Sub-agents are OpenClaw's answer to the "one thing at a time" limitation of AI conversations. Instead of making you wait while the AI writes a 3,000-line file, the main agent spawns a worker, stays responsive, and notifies you when the work is done. It's the same pattern as web workers in browsers or goroutines in Go โ delegate heavy work so the main thread stays snappy.
Full architecture diagram + when to customize what
Click any component to see what it does:
Now that you understand the architecture, here's your guide for where to make changes:
Create a channel plugin in src/channels/plugins/. Implement the adapter interfaces. The rest of the system works automatically.
Write a SKILL.md file and install it to ~/.openclaw/skills/. No code changes needed โ skills are pure Markdown.
Edit the YAML config with binding rules. The routing engine supports per-peer, per-guild, per-role, and per-channel bindings.
Add a provider config in YAML. OpenClaw supports 50+ providers through its OpenAI-compatible API layer, plus native integrations for Anthropic, Google, and more.
Use hooks in the config to inject custom behavior at various points in the auto-reply pipeline โ before/after tool calls, on message receive, etc.
You now understand how OpenClaw works โ from entry point to AI response. The codebase is open source, so go explore!