> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wednesdayai.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent tools

> Register custom agent tools from a plugin: TypeBox parameters, the execute() handler, optional tools, and allowlists.

# 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.

<Note>
  Tools are registered from a plugin's `register` function. If you have not built a plugin
  yet, start with [Write your first plugin](/developers/plugins/your-first-plugin) — this page
  goes deeper on the tool surface itself.
</Note>

## The tool shape

The object you pass to `api.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.                            |

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

```ts theme={"dark"}
import { Type } from "@sinclair/typebox";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

export default function register(api: OpenClawPluginApi): void {
  api.registerTool({
    name: "fetch_url",
    description: "Fetch the contents of a URL and return the text.",
    parameters: Type.Object({
      url: Type.String({ description: "The URL to fetch." }),
      timeoutMs: Type.Optional(
        Type.Number({ description: "Timeout in milliseconds. Default: 5000." }),
      ),
    }),
    async execute(_id, params) {
      const url = params.url as string;
      const timeoutMs = (params.timeoutMs as number | undefined) ?? 5000;
      const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
      if (!res.ok) {
        return {
          content: [{ type: "text", text: `HTTP ${res.status} from ${url}` }],
        };
      }
      const text = await res.text();
      return { content: [{ type: "text", text }] };
    },
  });
}
```

### 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:

```ts theme={"dark"}
return { content: [{ type: "text", text: "result here" }] };
```

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`:

```ts theme={"dark"}
import { jsonResult } from "openclaw/plugin-sdk";

async execute(_id, params) {
  return jsonResult({ ok: true, query: params.query });
}
```

## 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:

```ts theme={"dark"}
import { readStringParam, readNumberParam } from "openclaw/plugin-sdk";

async execute(_id, params) {
  const query = readStringParam(params, "query", { required: true });
  const limit = readNumberParam(params, "limit", { integer: true }) ?? 5;
  // ...
}
```

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:

| 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`:

```ts theme={"dark"}
import { Type } from "@sinclair/typebox";
import { stringEnum } from "openclaw/plugin-sdk";

const parameters = Type.Object({
  mode: stringEnum(["fast", "thorough"], { description: "Search mode." }),
});
```

## 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:

```ts theme={"dark"}
api.registerTool(
  {
    name: "workflow_tool",
    description: "Run a local workflow.",
    parameters: Type.Object({ pipeline: Type.String() }),
    async execute(_id, params) {
      return { content: [{ type: "text", text: params.pipeline as string }] };
    },
  },
  { optional: true },
);
```

Optional tools are never auto-enabled. Users enable them in their config under `tools.allow` (global) or `agents.list[].tools.allow` (per-agent):

```json5 theme={"dark"}
{
  tools: {
    allow: [
      "workflow_tool", // a specific tool name
      "my-plugin", // a plugin id — enables all tools from that plugin
      "group:plugins", // all plugin tools
    ],
  },
}
```

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.

```ts theme={"dark"}
api.registerTool({
  name: "danger_action",
  description: "Owner-only privileged action.",
  parameters: Type.Object({}),
  ownerOnly: true,
  async execute() {
    return { content: [{ type: "text", text: "done" }] };
  },
});
```

## 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:

```ts theme={"dark"}
api.registerTool((ctx) => {
  if (!ctx.agentId) return undefined;
  return {
    name: "scoped_tool",
    description: "A tool scoped to the current agent.",
    parameters: Type.Object({}),
    async execute() {
      return { content: [{ type: "text", text: `agent ${ctx.agentId}` }] };
    },
  };
});
```

## 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:

```ts theme={"dark"}
import { describe, expect, it } from "vitest";
import { Type } from "@sinclair/typebox";

it("returns text content", async () => {
  let captured: any;
  const api: any = { registerTool: (tool: any) => (captured = tool) };
  register(api);

  const result = await captured.execute("call-1", { url: "https://example.com" });
  expect(result.content[0].type).toBe("text");
});
```

```bash theme={"dark"}
pnpm --filter @wednesdayai/my-plugin test:fast
```

## 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](/developers/plugins/your-first-plugin) — end-to-end worked example
* [Plugin SDK reference](/developers/sdk) — the full exported surface
* [Hooks](/developers/hooks) — lifecycle callbacks via `api.on(...)`
