Custom Tool Creation
OpenClaw's built-in tools cover the most common automation tasks, but real workflows often require custom integrations. This guide shows you how to build type-safe, well-documented tools that agents can discover and use reliably.
Anatomy of a Tool
Every OpenClaw tool is a Python function decorated with @tool and documented with a clear docstring. The decorator handles registration, the docstring feeds the LLM's tool selection, and Python type annotations define the input schema automatically.
from openclaw.tools import tool
@tool
def get_stock_price(ticker: str, currency: str = "USD") -> dict:
"""
Get the current stock price for a publicly traded company.
Use this tool when you need current stock price data.
Do not use for cryptocurrency prices — use get_crypto_price instead.
Args:
ticker: Stock ticker symbol (e.g., AAPL, GOOGL, MSFT)
currency: Currency for price display. Default: USD
Returns:
dict with keys: ticker, price, currency, change_pct, timestamp
"""
import requests
response = requests.get(
f"https://your-finance-api.com/v1/quote/{ticker}",
params={"currency": currency},
timeout=10
)
response.raise_for_status()
return response.json()Notice the docstring quality — it tells the LLM when to use the tool, when NOT to use it, what the arguments mean, and what the return value looks like. Good docstrings dramatically improve tool selection accuracy.
Registering Tools
Tools must be registered with your agent before they're available at runtime. There are two registration methods:
# Method 1: Register via config (declarative)
tools:
- name: get_stock_price
module: my_tools.finance # Python module path
- name: send_slack_message
module: my_tools.integrations# Method 2: Register programmatically (useful for conditional tools)
from openclaw import Agent
from my_tools.finance import get_stock_price
agent = Agent.from_config("config.yaml")
agent.register_tool(get_stock_price)
result = agent.run("What is the current stock price of Apple?")
print(result)Tool Safety and Sandboxing
Tools that modify state (write files, call external APIs, execute code) need careful safety controls. OpenClaw provides built-in guards, and you should add domain-specific validation in your tool implementation.
from openclaw.tools import tool, requires_confirmation
import pathlib
@tool
@requires_confirmation # agent must confirm before this tool is called
def write_file(path: str, content: str) -> str:
"""
Write content to a file on the local filesystem.
Only use for files in the project working directory.
Never write to system paths like /etc, /usr, or C:\Windows.
Args:
path: Relative file path from the working directory
content: Content to write to the file
"""
# Security: prevent path traversal
safe_base = pathlib.Path.cwd()
target = (safe_base / path).resolve()
if not str(target).startswith(str(safe_base)):
raise PermissionError(f"Path {path!r} is outside working directory")
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding='utf-8')
return f"Written {len(content)} chars to {path}"Async Tools for I/O-Bound Operations
Tools that make network requests or perform I/O benefit from async implementation when running in parallel agent contexts. OpenClaw supports both sync and async tools natively.
import aiohttp
from openclaw.tools import tool
@tool
async def fetch_json(url: str, headers: dict = None) -> dict:
"""
Fetch JSON data from a URL. Use for REST API calls that return JSON.
Args:
url: Full URL to fetch (must be HTTPS for external URLs)
headers: Optional HTTP headers as key-value pairs
"""
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers or {}, timeout=aiohttp.ClientTimeout(total=30)) as resp:
resp.raise_for_status()
return await resp.json()Testing Custom Tools
Tools should be tested independently before being used by agents. Unit tests for tools are straightforward since they're just Python functions.
import pytest
from unittest.mock import patch, MagicMock
from my_tools.finance import get_stock_price
def test_get_stock_price_success():
mock_response = {"ticker": "AAPL", "price": 175.42, "currency": "USD",
"change_pct": 0.32, "timestamp": "2024-01-15T16:00:00Z"}
with patch("requests.get") as mock_get:
mock_get.return_value = MagicMock(status_code=200)
mock_get.return_value.json.return_value = mock_response
result = get_stock_price("AAPL")
assert result["ticker"] == "AAPL"
assert result["price"] == 175.42
def test_get_stock_price_invalid_ticker():
with patch("requests.get") as mock_get:
mock_get.return_value = MagicMock(status_code=404)
mock_get.return_value.raise_for_status.side_effect = Exception("404")
with pytest.raises(Exception):
get_stock_price("INVALID123")You can also test tools in the context of an agent by using the --dry-run flag, which shows which tools the agent would call for a given prompt without actually executing them. This is valuable for verifying tool selection accuracy before running the full pipeline.
Tool Discoverability and Naming
One of the most impactful factors for multi-tool agents is how well the LLM can choose the right tool for each step. The tool's name and docstring are the primary signals. Follow these naming conventions to maximize discoverability:
- Use verb_noun format:
search_web,read_file,send_email,query_database - Be specific, not generic:
search_github_issuesis better thansearchwhen you have multiple search tools - Name edge cases in docstrings: "Use this for X, not for Y" helps agents make better selections in ambiguous situations
- Group related tools by prefix:
github_list_issues,github_create_issue,github_close_issue— the common prefix helps agents understand the tool is suited to GitHub operations
Tool Versioning and Updates
When you update a tool's signature or behavior, existing agents using that tool may behave unexpectedly. Follow these practices for safe tool evolution:
Add new parameters as optional with sensible defaults rather than changing required parameters. If you need a breaking change, create a new tool name (e.g., get_stock_price_v2) rather than modifying the existing one — this prevents silent regressions in running agents. Deprecate old tools in their docstring with a migration note.
@tool
def get_stock_price(ticker: str, currency: str = "USD") -> dict:
"""
[DEPRECATED — use get_market_data instead which includes more fields]
Get the current stock price for a publicly traded company...
"""Tool Performance and Timeouts
Slow tools create a poor user experience and can cause agent timeouts. Every tool that makes a network call should have an explicit timeout, and tools that might return large responses should implement pagination or size limits. OpenClaw enforces a default 30-second timeout per tool call; tools that need longer (bulk data fetching, code compilation) must declare an explicit timeout override in their registration config. For tools called in parallel agent contexts, fast, focused tools dramatically outperform monolithic ones — a tool that does one thing in 200ms is more composable than one that does five things in 3 seconds.