Architecture at a Glance

OpenClaw follows a layered architecture that separates concerns cleanly, making every layer independently testable and replaceable. The Provider Abstraction normalises OpenAI, Anthropic, Groq, and Ollama behind a common BaseLLM interface. The Agent Core runs the think/act loop, managing iterations, tool dispatch, and memory hydration. The Tool Registry holds all built-in and custom tools discovered at startup. The Event Bus fires lifecycle hooks consumed by plugins and monitoring middleware.

Core Components

ComponentModuleResponsibilityExtension Point
Agentopenclaw.agentThink/act loop, iteration cap, timeoutSubclass BaseAgent
Provideropenclaw.providersLLM API abstraction, retry, streamingImplement BaseLLM
Tool Registryopenclaw.toolsTool discovery, schema generation, dispatch@tool decorator
Memoryopenclaw.memoryShort/long-term store, retrieval, evictionImplement BaseMemory
Event Busopenclaw.eventsLifecycle hooks, plugin communication@on_event decorator
Configopenclaw.configYAML/env merge, schema validation, defaultsCustom config sections

Sections

Quick Start: Contributing

# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/openclaw
cd openclaw

# 2. Create virtual environment
python -m venv .venv && source .venv/bin/activate

# 3. Install in dev mode
pip install -e ".[dev]"

# 4. Install pre-commit hooks
pre-commit install

# 5. Run tests to verify setup
pytest tests/ -v --tb=short

Python API Quick Example

import openclaw

agent = openclaw.Agent(
    provider="openai",
    model="gpt-4o-mini",
    tools=["web_search", "read_file"]
)

result = agent.run("Summarize the latest news on AI regulation")
print(result.output)

Writing a Custom Tool

Tools are the atomic actions available to an agent. Use the @tool decorator to register any Python function as a tool. The decorator introspects the type hints and docstring to generate the JSON schema automatically β€” no manual schema writing required.

from openclaw.tools import tool
from typing import Optional

@tool
def fetch_stock_price(ticker: str, currency: str = "USD") -> dict:
    """Fetch the latest stock price for a given ticker symbol.

    Args:
        ticker: Stock ticker symbol, e.g. 'AAPL' or 'TSLA'.
        currency: Currency code for the price. Defaults to 'USD'.

    Returns:
        dict with keys: ticker, price, currency, timestamp.
    """
    # implementation here
    import yfinance as yf
    data = yf.Ticker(ticker).fast_info
    return {
        "ticker": ticker,
        "price": data.last_price,
        "currency": currency,
        "timestamp": data.last_trade_date.isoformat(),
    }

Then pass it to the agent:

from your_tools import fetch_stock_price

agent = openclaw.Agent(
    provider="openai",
    model="gpt-4o",
    tools=[fetch_stock_price, "web_search"]  # mix custom and built-in
)
print(agent.run("What is today's Apple stock price?").output)

Implementing a Custom LLM Provider

To add a provider not natively supported (e.g., a private LLM endpoint), subclass BaseLLM and implement the two required methods:

from openclaw.providers import BaseLLM, LLMResponse
from typing import Iterator

class MyPrivateLLM(BaseLLM):
    """Provider for an internal fine-tuned model behind a private API."""

    def complete(self, prompt: str, **kwargs) -> LLMResponse:
        response = self._call_internal_api(prompt, stream=False)
        return LLMResponse(
            content=response["text"],
            tokens_used=response["usage"]["total_tokens"],
            model=self.model_name,
        )

    def stream(self, prompt: str, **kwargs) -> Iterator[str]:
        for chunk in self._call_internal_api(prompt, stream=True):
            yield chunk["delta"]

    def _call_internal_api(self, prompt: str, stream: bool) -> dict:
        import requests
        return requests.post(
            self.config.endpoint,
            json={"prompt": prompt, "stream": stream},
            headers={"X-API-Key": self.config.api_key},
            timeout=60,
        ).json()

Plugin & Event System

OpenClaw's event bus fires hooks at every stage of an agent run. You can subscribe to events to add custom logging, cost tracking, human-in-the-loop checkpoints, or other middleware without touching core agent code.

EventFired WhenPayload Fields
agent.startedAgent begins a taskagent_id, task, config
agent.iterationEach think/act loop iterationiteration, thought, tool_call
tool.calledBefore a tool executestool_name, args, iteration
tool.resultAfter a tool returnstool_name, result, duration_ms
llm.requestBefore each LLM API callprompt_tokens, model
llm.responseAfter each LLM API responsecompletion_tokens, total_tokens, duration_ms
agent.completedTask finishes successfullyoutput, total_tokens, elapsed_s
agent.failedTask fails or times outerror, iteration, elapsed_s
from openclaw.events import on_event

@on_event("llm.response")
def track_token_cost(event):
    cost = event.total_tokens * 0.000002  # $0.002 / 1K tokens
    print(f"Task {event.task_id}: ${cost:.4f} ({event.total_tokens} tokens)")

Code Style & Convention

OpenClaw contributions must pass the CI linting gate. The toolchain is:

  • Ruff β€” linting and import sorting (ruff check . --fix)
  • Black β€” opinionated code formatting (black src/ tests/)
  • Mypy β€” static type checking in strict mode (mypy src/openclaw)
  • pre-commit β€” runs all checks automatically on each commit

All public functions and classes must have type annotations. Docstrings follow the Google style format. Commit messages follow Conventional Commits: feat:, fix:, docs:, chore:.

Pull Request Checklist

  • βœ… Tests added or updated for the changed code; coverage does not decrease
  • βœ… ruff, black, and mypy pass with zero errors
  • βœ… CHANGELOG.md entry added under Unreleased
  • βœ… Documentation updated in docs/ for any public API change
  • βœ… Breaking changes noted in PR description with migration path
  • βœ… PR description answers: What? / Why? / How tested?