Agent tools
Agent tools are actions the AI can invoke during a run. A plugin registers a tool throughapi.registerTool(...); the runtime exposes it to the model, validates input against your schema, calls your execute() handler, and returns the result to the model.
Tools are registered from a plugin’s
register function. If you have not built a plugin
yet, start with Write your first plugin — this page
goes deeper on the tool surface itself.The tool shape
The object you pass toapi.registerTool(...) is an AnyAgentTool. The fields that matter for a typical tool are:
There is no
ToolDefinition, no inputSchema, and no run() — those are not part of the WednesdayAI surface. The real type is AnyAgentTool, re-exported from openclaw/plugin-sdk.
Defining a tool
The execute signature
execute(id, params) receives:
id— the tool-call id assigned by the runtime. Most tools ignore it (_id).params— the validated input object matching yourparametersschema.
The return value
execute returns a content result. The common shape is a single text block:
content array may include text and image blocks. For JSON payloads, use the jsonResult(...) helper from the SDK, which stringifies the payload into a text block and attaches structured details:
Reading parameters safely
Model-supplied params are untrusted. The SDK exposes typed readers that coerce and validate input and throw a clear error on bad values:snake_case variants of camelCase keys, so timeoutMs resolves from either timeoutMs or timeout_ms.
Schema rules
These constraints apply to every toolparameters schema:
stringEnum and optionalStringEnum are exported from openclaw/plugin-sdk:
Optional tools (opt-in)
By default, a registered tool is available to all agents. If your tool makes outbound requests, uses a paid API, or has side effects, register it as optional so operators opt in explicitly:tools.allow (global) or agents.list[].tools.allow (per-agent):
Owner-only tools
SetownerOnly: true on the tool object to restrict execution to owner senders. The runtime wraps non-owner calls so they fail with a restriction error before execute runs.
Tool factory context
The full set of fields available in a tool factory context (OpenClawPluginToolContext):
Tool factories
Instead of a static tool, you can pass a factory that receives per-invocation context (agent id, session key, workspace dir, sender identity) and returns a tool, an array of tools, or nothing:Naming and conflicts
- Tool names are not deduplicated at registration time. Two tools sharing a name will both register; runtime resolution order decides which is called. Use a unique, namespaced name (for example
my-plugin_fetch_url). - A plugin tool name must not clash with a core tool name — conflicting tools are skipped.
Testing tools
Register the tool against a minimal fakeapi and call execute directly — no full gateway needed:
Testing the real tool pipeline
The fake-api approach above unit-tests execute() in isolation. To test the
pipeline the gateway actually runs — plugin discovery, factory resolution,
allowlist filtering, and the model-facing tool definition — import the testing
seam:
- Discovery —
loadOpenClawPlugins({ cache: false, workspaceDir, config })with explicitplugins.enabled: true,plugins.allow, andplugins.load.pathspointing at fixture plugin files (a.cjsentry plus anopenclaw.plugin.jsonmanifest). Returns the real plugin registry. - Resolution + filtering —
resolvePluginTools({ context })invokes each registered tool factory and appliesrunToolAllowlist(and the optional-toolallowpolicy). One documented quirk: entries registered as bare factories carry no tool names up front, so arunToolAllowlistpre-filter skips them even when their resolved tool name matches — register tools with names if you rely on that filter. - Adaptation —
toToolDefinitions(tools)produces the model-facing definitions:labelfalls back tonamewhen unset, thrown errors become structured error results, andexecute(toolCallId, params, signal, onUpdate)passes theAbortSignalas the third argument — never a host-context object.
loadOpenClawPlugins activates the global plugin registry. Reset it between
tests so suites stay order-independent:
PluginLoadOptions, PluginRegistry, PluginToolRegistration, and
HookContext are exported as types for annotating fixtures.
Best practices
- Single responsibility — one tool per well-defined action.
- Clear descriptions — the model chooses tools from the description; be specific about what and when.
- Validate input — use the SDK param readers; never trust raw model output.
- Return helpful errors — return a text result describing the problem rather than throwing for expected failures.
- Mark side-effecting tools optional — give operators explicit control.
What’s next
- Write your first plugin — end-to-end worked example
- Plugin SDK reference — the full exported surface
- Hooks — lifecycle callbacks via
api.on(...)