OpenClaw Architecture
Understanding OpenClaw's internal architecture helps you extend it effectively, contribute confidently, and debug issues at the appropriate layer. This guide covers the core components, their responsibilities, and how they interact.
High-Level Overview
OpenClaw is organized as a layered system with clear separation between the user interface layer, the orchestration layer, and the execution layer:
┌─────────────────────────────────────────────┐
│ Interface Layer │
│ CLI (click) │ REST API (FastAPI) │ Python SDK│
├─────────────────────────────────────────────┤
│ Orchestration Layer │
│ AgentRunner │ PipelineEngine │ Scheduler │
├─────────────────────────────────────────────┤
│ Core Layer │
│ LLMRouter │ ToolRegistry │ MemoryManager │
├─────────────────────────────────────────────┤
│ Storage Layer │
│ VectorStore │ EventStore │ ConfigStore │
└─────────────────────────────────────────────┘Each layer depends only on layers below it, never above. The CLI and REST API are both thin wrappers over the same orchestration primitives — there is no separate "CLI codepath" and "API codepath." This uniformity ensures that behavior is consistent regardless of how the agent is invoked.
Core Components
AgentRunner
The central component that manages a single agent's execution loop. Given a task and configuration, AgentRunner: initializes the LLMRouter for model selection, loads the ToolRegistry for available tools, hydrates MemoryManager with relevant context, and executes the ReAct reasoning loop until the task is complete or budget is exhausted.
Key responsibility: ensuring the agent makes progress on every iteration and terminates cleanly when done, rather than looping indefinitely.
LLMRouter
Abstracts LLM providers behind a unified interface. Handles authentication, retry with exponential backoff, model fallback (if primary model is unavailable, route to secondary), rate limiting, token counting, and cost tracking. Adding a new LLM provider means implementing the LLMProvider protocol and registering it — all routing logic is inherited automatically.
ToolRegistry
Manages tool discovery, registration, and invocation. Generates the JSON Schema tool descriptions sent to the LLM for tool selection. Handles input validation against schemas, timeout enforcement, sandboxing, and structured error capture for tool failures.
MemoryManager
Provides a unified interface to all memory backends (working, episodic, semantic, procedural). Handles context assembly — merging retrieved memories into the agent's context window in the correct format for each memory type, and managing context window budget allocation across memory sources.
Request Lifecycle
Tracing a request from CLI command to final output illustrates how all components interact:
1. CLI: openclaw run "task"
└─ Parses args → loads config → creates AgentRunner
2. AgentRunner.execute(task)
├─ MemoryManager.hydrate() → retrieve relevant context
├─ LLMRouter.complete(messages + context + tools)
│ └─ calls LLM API with tool schemas in messages
└─ LLM responds with either:
a) Tool call → ToolRegistry.invoke(tool, args)
└─ tool runs → result appended to context
└─ loop back to step 2 (ReAct loop)
b) Final text answer → task complete
3. Output captured → written to file or stdout
4. Metrics recorded to Prometheus registry
5. Interaction saved to MemoryManager (if episodic/working)Extension Points
OpenClaw is designed to be extended at every layer without modifying core code. The primary extension points are:
| Extension point | How to extend | Use case |
|---|---|---|
| LLM providers | Implement LLMProvider protocol | Custom or private LLM endpoints |
| Tools | @tool decorator + registration | Domain-specific actions |
| Memory backends | Implement MemoryBackend protocol | Custom databases, proprietary stores |
| Pipeline stages | Custom StageProcessor classes | Non-LLM pipeline steps (data transforms) |
| Trigger sources | Implement TriggerSource protocol | New event sources (custom queues, IoT, etc.) |
| Output formatters | Custom OutputFormatter classes | Domain-specific output formats (PDF, DOCX, etc.) |
Configuration Loading and Resolution
Configuration is loaded in priority order (higher overrides lower):
- CLI flags (highest priority)
- Environment variables (
OPENCLAW_*prefix) - Project config file (
./openclaw.yaml) - User config file (
~/.openclaw/config.yaml) - Built-in defaults (lowest priority)
This allows global defaults in user config, project-specific overrides in project config, environment-specific settings in environment variables, and per-run overrides via CLI flags — the standard 12-factor app configuration hierarchy.
Data Flow: Memory and Context Assembly
Understanding exactly how context is assembled before each LLM call helps when debugging unexpected agent behavior. Before every LLM call in the ReAct loop, MemoryManager assembles context from all configured memory sources and appends it to the messages array in a specific order:
First, the system prompt is placed at the beginning of the messages array. Second, any working memory (conversation history) is appended in chronological order. Third, retrieved semantic memory chunks are inserted as a system message labeled [RELEVANT KNOWLEDGE]. Fourth, retrieved episodic memories are inserted as a system message labeled [PAST INTERACTIONS]. Finally, the current user task is appended as the final user message.
This ordering is intentional — the system prompt has the highest positional authority, and the current task is always the final message to ensure it's the most salient signal for the LLM's next action. Changing this ordering can significantly affect agent behavior, which is why it's part of the core architecture rather than being user-configurable.
Tool Execution and Sandboxing
The ToolRegistry doesn't just store tool implementations — it manages their safe execution. When the LLM requests a tool call, ToolRegistry validates the arguments against the tool's JSON Schema, checks whether the tool requires confirmation (and if so, requests it from the appropriate handler), applies the configured timeout, executes the tool in a subprocess or thread pool depending on the tool's declared execution model, captures stdout/stderr and any exceptions, and returns a structured result with either the tool's return value or a sanitized error description.
Tools that require file system or network access run in a context with explicit permissions — the agent config declares which directories are writable and which domains are reachable. Attempts to access resources outside the declared permissions raise a PermissionDenied error that the LLM can reason about and handle gracefully rather than an opaque system error.
Concurrency Model
OpenClaw uses Python's asyncio event loop as its concurrency primitive. The REST API, webhook handlers, queue consumers, and pipeline parallel stages all run in the same event loop, coordinated by asyncio's cooperative multitasking. CPU-bound tools run in a ProcessPoolExecutor to avoid blocking the event loop; I/O-bound tools run as coroutines.
The practical implication for plugin developers: tool implementations should be declared as async def when they perform I/O (HTTP requests, database queries, file reads), and as regular def only for purely computational work. Synchronous I/O in tools (blocking requests.get() calls, for example) will block the entire event loop and reduce system throughput under concurrent load.