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

# Write your first plugin

> Build a WednesdayAI plugin that adds a new agent tool — from scaffolding to a working local install.

# Write your first plugin

WednesdayAI's core is intentionally small. Features that are specific to a use case, platform, or workflow belong in plugins — not in core. This keeps the core stable and the system extensible without requiring a fork.

A plugin is a Node.js package whose `register(api)` function attaches one or more capabilities through the plugin SDK. This guide builds a plugin with one agent tool. It covers both manifest forms, the SDK surface, async safety, optional tools, config schema, and local testing.

## Prerequisites

* Node.js ≥ 24.12.0
* A running WednesdayAI instance for testing
* TypeScript familiarity — the plugin SDK is fully typed

## Extension points overview

Before writing code, understand where plugins touch the system. Each capability is attached with a dedicated method on the `OpenClawPluginApi` object passed to your `register` function:

| Extension point | What it owns                                                                     | Register with                |
| --------------- | -------------------------------------------------------------------------------- | ---------------------------- |
| Agent tool      | An action the AI can call (search, fetch, compute)                               | `api.registerTool(...)`      |
| Lifecycle hook  | A typed runtime callback (transform LLM input/output, gate tools, shape context) | `api.on(...)`                |
| Channel adapter | A new chat platform                                                              | `api.registerChannel(...)`   |
| HTTP route      | A custom endpoint on the gateway                                                 | `api.registerHttpRoute(...)` |
| Provider        | A model provider and its auth methods                                            | `api.registerProvider(...)`  |
| Command         | A chat command that bypasses the LLM                                             | `api.registerCommand(...)`   |

This guide builds a tool. For the other extension points, see [Agent tools](/developers/agent-tools), [Hooks](/developers/hooks), [Channel adapters](/developers/channel-adapters), and the [Plugin SDK reference](/developers/sdk) (which also covers providers, commands, services, gateway methods, session-store adapters, and web-search providers).

## Step 1: Scaffold the plugin

Create a directory for your plugin. The directory name must match the plugin `id` exactly.

```bash theme={"dark"}
mkdir my-tool
cd my-tool
npm init -y
npm install --save-dev typescript @types/node
npm install --save-dev openclaw@2026.3.2   # pin to the WednesdayAI fork base version
```

Add `"type": "module"` to `package.json` — the plugin SDK uses ESM:

```json theme={"dark"}
{
  "name": "my-tool",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js"
}
```

Create `tsconfig.json`:

```json theme={"dark"}
{
  "compilerOptions": {
    "target": "es2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "declaration": true
  },
  "include": ["src"]
}
```

## Step 2: Write the manifest

Create `openclaw.plugin.json` at the plugin root. This file is **required** — the runtime validates plugin config against it **without executing your code**, so a missing or invalid manifest blocks loading.

The manifest is plain JSON (not JSON5):

```json theme={"dark"}
{
  "id": "my-tool",
  "name": "My Tool",
  "description": "A plugin that adds a custom agent tool",
  "version": "1.0.0",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "apiKey": {
        "type": "string",
        "description": "API key for the external service"
      }
    },
    "required": ["apiKey"]
  },
  "uiHints": {
    "apiKey": {
      "label": "API Key",
      "sensitive": true
    }
  }
}
```

**Key rules:**

* `id` must match the directory name exactly.
* `configSchema` is required. If your plugin has no config, use `{ "type": "object", "additionalProperties": false }`.
* `additionalProperties: false` ensures typos in user config are caught, not silently ignored.
* The manifest is plain JSON — do not add comments or trailing commas.
* Declaring a field in `required` means the runtime rejects the plugin config at load time if the field is absent. If you also guard for a missing value inside `execute()`, that guard is belt-and-suspenders for deployments where config validation may be bypassed (e.g. direct API calls). Both are fine to have together.

<Note>
  **`openclaw.plugin.json` vs the `package.json` `"openclaw"` key.** These are two different things.
  `openclaw.plugin.json` is the **manifest** — it carries `id` + `configSchema` and drives config
  validation. The `package.json` `"openclaw"` key is used for **workspace/pack discovery** (the `id`,
  or an `extensions` array for a multi-plugin pack). A published plugin typically ships both. See the
  [manifest reference](/developers/plugins/manifest).
</Note>

## Step 3: Write the plugin entry point

The plugin's entry exports a default `register` function. This function is called by the runtime when the plugin is loaded.

Create `src/index.ts`:

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

export default function register(api: OpenClawPluginApi): void {
  api.registerTool({
    name: "my_tool",
    description: "Describe what this tool does — the AI reads this to decide when to call it.",
    parameters: Type.Object({
      query: Type.String({ description: "The input to process" }),
    }),
    async execute(_id, params) {
      const result = `Processed: ${params.query}`;
      return {
        content: [{ type: "text", text: result }],
      };
    },
  });
}
```

**Points to note:**

* Import from `openclaw/plugin-sdk`, not from relative paths into core. Relative core imports (`../../src/...`) work in bundled extensions but break in installed plugins.
* Tool names are not deduplicated at registration time. If two tools share a name, both are registered and runtime resolution order determines which is called. Use a unique, namespaced name (e.g. `my-plugin_my_tool`) to avoid conflicts.

## Async safety

<Warning>
  The `register()` function is called at startup. Any long-running blocking operation inside it
  delays gateway readiness.
</Warning>

The `register` function may be synchronous or async. Use async if you need to perform startup I/O (reading a config file, pinging an API). The key constraint is: don't block indefinitely.

**Tool `execute()` functions are always async** and run when the AI decides to call the tool. A slow `execute()` does not block the gateway — but it does block the user's reply until it completes.

**Do not do this:**

```typescript theme={"dark"}
export default function register(api: OpenClawPluginApi): void {
  // Synchronous I/O blocks the event loop during startup
  const config = JSON.parse(fs.readFileSync("./data.json", "utf8"));
  api.registerTool({ ... });
}
```

**Do this instead:**

```typescript theme={"dark"}
export default async function register(api: OpenClawPluginApi): Promise<void> {
  // Async I/O during startup — does not block the event loop
  const config = JSON.parse(await fs.promises.readFile("./data.json", "utf8"));
  api.registerTool({ ... });
}
```

**For long-running tool operations:** Return a progress message immediately if the tool makes a network request that could take more than a few seconds. Set user expectations in the tool `description` field.

## Step 4: Register as optional

By default, registered tools are available to all agents. If your tool is not universally useful (makes outbound network requests, uses a paid API, has side effects), register it as optional:

```typescript theme={"dark"}
api.registerTool(
  {
    name: "my_tool",
    description: "...",
    parameters: Type.Object({ query: Type.String() }),
    async execute(_id, params) {
      /* ... */
    },
  },
  { optional: true }, // user must explicitly enable this tool
);
```

Users enable an optional tool via the top-level `tools.allow` key in their WednesdayAI config:

```json5 theme={"dark"}
{
  tools: {
    allow: ["my_tool"],
  },
}
```

<Tip>
  Mark tools as optional whenever they make outbound requests, use credentials, or have meaningful
  side effects. This gives operators explicit control over what the AI can do.
</Tip>

## Step 5: Access plugin config in execute

If your manifest declares a `configSchema`, users set values under the plugin's `id` in their WednesdayAI config. The config is available on the `api` object passed to `register`. Capture it in a closure so `execute()` can access it:

```typescript theme={"dark"}
export default function register(api: OpenClawPluginApi): void {
  // api.pluginConfig is Record<string, unknown> | undefined
  const pluginConfig = api.pluginConfig;

  api.registerTool({
    name: "my_tool",
    description: "...",
    parameters: Type.Object({ query: Type.String() }),
    async execute(_id, params) {
      const apiKey = pluginConfig?.apiKey as string | undefined;
      if (!apiKey) {
        return {
          content: [{ type: "text", text: "Plugin not configured: apiKey is missing." }],
        };
      }
      // use apiKey ...
      return { content: [{ type: "text", text: `Done: ${params.query}` }] };
    },
  });
}
```

Always handle missing config gracefully — return a helpful message rather than throwing.

## Step 6: Build and install locally

```bash theme={"dark"}
# Build
npx tsc

# Pack
npm pack
# Produces: my-tool-1.0.0.tgz

# Install into WednesdayAI
openclaw plugins install ./my-tool-1.0.0.tgz

# Verify it loaded
openclaw plugins list
```

Expected output from `openclaw plugins list`:

```text theme={"dark"}
my-tool  1.0.0  loaded
```

## Step 7: Test it

Send a message to your WednesdayAI bot that should invoke the tool. If you registered it as optional, first add `"my_tool"` to `tools.allow` in your WednesdayAI config (see Step 4).

To watch tool calls in the logs on Linux:

```bash theme={"dark"}
journalctl --user -u openclaw-gateway -f | grep my_tool
```

Expected: a log line showing the tool being called with your params and a successful return.

## Other extension points

Once you are comfortable registering a tool, the same `register(api)` entry attaches everything else:

* **Lifecycle hooks** — `api.on(name, handler)` to transform LLM input/output, gate tool calls, or shape context. See [Hooks](/developers/hooks).
* **Channel adapters** — `api.registerChannel(...)` to add a new messaging platform. See [Channel adapters](/developers/channel-adapters).
* **HTTP routes and gateway methods** — `api.registerHttpRoute(...)` / `api.registerGatewayMethod(...)`.
* **Commands and CLI** — `api.registerCommand(...)` for chat commands that bypass the LLM; `api.registerCli(...)` for CLI subcommands.
* **Services** — `api.registerService(...)` for start/stop background work.
* **Providers, session stores, web search** — `api.registerProvider(...)`, `api.registerSessionStoreAdapter(...)`, `api.registerWebSearchProvider(...)`.

The full surface is documented in the [Plugin SDK reference](/developers/sdk).

***

## What's next

* [Plugin manifest reference](/developers/plugins/manifest) — all manifest fields and their effects
* [Plugin SDK reference](/developers/sdk) — full exported surface with types
* [Agent tools](/developers/agent-tools) — go deeper on the tool surface
* [Hooks](/developers/hooks) — lifecycle callbacks for context, LLM input/output, and model call events
* [Channel adapters](/developers/channel-adapters) — add a new chat platform
* [Publishing a plugin](/developers/plugins/publishing) — the managed plugin lifecycle
