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:
| Field | Type | Notes |
|---|---|---|
name | string | The tool name the model sees. Use a namespaced, unique name. |
description | string | The model reads this to decide when to call the tool. Be specific. |
parameters | TypeBox schema | Input schema (Type.Object({ ... })). |
execute | (id, params) => Promise<result> | Your handler. Returns a content result. |
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:
| Rule | Why |
|---|---|
No Type.Union | Structural unions confuse model tool use. Use separate fields or tools. |
No anyOf / oneOf / allOf | Same reason. |
Use Type.Optional(...) not T | null | null is not the WednesdayAI convention. |
Use stringEnum / optionalStringEnum for enums | These produce a Type.Unsafe enum the validators accept. |
Never use format as a raw property name | Some validators reject it. |
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):
| Field | Type | Description |
|---|---|---|
config | OpenClawConfig | undefined | The resolved gateway config |
workspaceDir | string | undefined | Workspace directory path for the current agent |
agentDir | string | undefined | Agent-specific directory path |
agentId | string | undefined | The agent id that is running |
sessionKey | string | undefined | Unique session key (e.g. agent:id:telegram:dm:123) |
sessionId | string | undefined | Ephemeral session UUID — regenerated on /new and /reset; use for per-conversation isolation |
messageChannel | string | undefined | The channel surface ("telegram", "discord", "whatsapp", etc.) |
agentAccountId | string | undefined | The channel account id the message arrived on |
requesterSenderId | string | undefined | Trusted sender id from inbound context (runtime-provided, not tool args) |
senderIsOwner | boolean | undefined | Whether the trusted sender is an owner |
sandboxed | boolean | undefined | Whether the gateway is running in sandbox mode |
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:
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(...)