Skip to main content

Agent tools

Agent tools are actions the AI can invoke during a run. A plugin registers a tool through api.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 to api.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 your parameters schema.

The return value

execute returns a content result. The common shape is a single text block:
The 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:
These readers also accept snake_case variants of camelCase keys, so timeoutMs resolves from either timeoutMs or timeout_ms.

Schema rules

These constraints apply to every tool parameters 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:
Optional tools are never auto-enabled. Users enable them in their config under tools.allow (global) or agents.list[].tools.allow (per-agent):
An allowlist that names only plugin tools is treated as plugin opt-in: core tools stay enabled unless you also list core tools or groups.

Owner-only tools

Set ownerOnly: 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 fake api 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:
The subpath is supported for test code only — never import it from production plugin code, and it is never loaded by the runtime loader. (This is a deliberate divergence from upstream openclaw, which keeps its testing barrel repo-local.) Three steps:
  1. DiscoveryloadOpenClawPlugins({ cache: false, workspaceDir, config }) with explicit plugins.enabled: true, plugins.allow, and plugins.load.paths pointing at fixture plugin files (a .cjs entry plus an openclaw.plugin.json manifest). Returns the real plugin registry.
  2. Resolution + filteringresolvePluginTools({ context }) invokes each registered tool factory and applies runToolAllowlist (and the optional-tool allow policy). One documented quirk: entries registered as bare factories carry no tool names up front, so a runToolAllowlist pre-filter skips them even when their resolved tool name matches — register tools with names if you rely on that filter.
  3. AdaptationtoToolDefinitions(tools) produces the model-facing definitions: label falls back to name when unset, thrown errors become structured error results, and execute(toolCallId, params, signal, onUpdate) passes the AbortSignal as 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