Files
openclaw/docs/plugins/building-plugins.md
Pablo Guardiola 0c867eef75 feat: expose plugin approval action metadata
Expose plugin approval action metadata so plugins can describe richer approval actions across gateway, SDK, channel, and UI surfaces.
2026-05-26 22:46:09 -07:00

17 KiB

summary, title, sidebarTitle, doc-schema-version, read_when
summary title sidebarTitle doc-schema-version read_when
Create your first OpenClaw plugin in minutes Building plugins Getting Started 1
You want to create a new OpenClaw plugin
You need a quick-start for plugin development
You are choosing between channel, provider, CLI backend, tool, or hook docs

Plugins extend OpenClaw without changing core. A plugin can add a messaging channel, model provider, local CLI backend, agent tool, hook, media provider, or another plugin-owned capability.

You do not need to add an external plugin to the OpenClaw repository. Publish the package to ClawHub and users install it with:

openclaw plugins install clawhub:<package-name>

Bare package specs still install from npm during the launch cutover. Use the clawhub: prefix when you want ClawHub resolution.

Requirements

  • Use Node 22.19 or newer and a package manager such as npm or pnpm.
  • Be familiar with TypeScript ESM modules.
  • For in-repo bundled plugin work, clone the repository and run pnpm install. Source-checkout plugin development is pnpm-only because OpenClaw loads bundled plugins from extensions/* workspace packages.

Choose the plugin shape

Connect OpenClaw to a messaging platform. Add a model, media, search, fetch, speech, or realtime provider. Run a local AI CLI through OpenClaw model fallback. Register agent tools.

Quickstart

Build a minimal tool plugin by registering one required agent tool. This is the shortest useful plugin shape and shows the package, manifest, entry point, and local proof.

{
  "name": "@myorg/openclaw-my-plugin",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.24-beta.2",
      "minGatewayVersion": "2026.3.24-beta.2"
    },
    "build": {
      "openclawVersion": "2026.3.24-beta.2",
      "pluginSdkVersion": "2026.3.24-beta.2"
    }
  }
}
{
  "id": "my-plugin",
  "name": "My Plugin",
  "description": "Adds a custom tool to OpenClaw",
  "contracts": {
    "tools": ["my_tool"]
  },
  "activation": {
    "onStartup": true
  },
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}
</CodeGroup>

Published external plugins should point runtime entries at built JavaScript
files. See [SDK entry points](/plugins/sdk-entrypoints) for the full entry
point contract.

Every plugin needs a manifest, even when it has no config. Runtime tools
must appear in `contracts.tools` so OpenClaw can discover ownership without
eagerly loading every plugin runtime. Set `activation.onStartup`
intentionally. This example starts on Gateway startup.

For every manifest field, see [Plugin manifest](/plugins/manifest).
```typescript index.ts import { Type } from "typebox"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
  id: "my-plugin",
  name: "My Plugin",
  description: "Adds a custom tool to OpenClaw",
  register(api) {
    api.registerTool({
      name: "my_tool",
      description: "Echo one input value",
      parameters: Type.Object({ input: Type.String() }),
      async execute(_id, params) {
        return {
          content: [{ type: "text", text: `Got: ${params.input}` }],
        };
      },
    });
  },
});
```

Use `definePluginEntry` for non-channel plugins. Channel plugins use
`defineChannelPluginEntry`.
For an installed or external plugin, inspect the loaded runtime:
```bash
openclaw plugins inspect my-plugin --runtime --json
```

If the plugin registers a CLI command, run that command too. For example,
a demo command should have an execution proof such as
`openclaw demo-plugin ping`.

For a bundled plugin in this repository, OpenClaw discovers source-checkout
plugin packages from the `extensions/*` workspace. Run the closest targeted
test:

```bash
pnpm test -- extensions/my-plugin/
pnpm check
```
Validate the package before publishing:
```bash
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
```

The canonical ClawHub snippets live in `docs/snippets/plugin-publish/`.
Install the published package through ClawHub:
```bash
openclaw plugins install clawhub:your-org/your-plugin
```

Plugin capabilities

A single plugin can register any number of capabilities via the api object:

Capability Registration method Detailed guide
Text inference (LLM) api.registerProvider(...) Provider Plugins
CLI inference backend api.registerCliBackend(...) CLI Backend Plugins
Channel / messaging api.registerChannel(...) Channel Plugins
Speech (TTS/STT) api.registerSpeechProvider(...) Provider Plugins
Realtime transcription api.registerRealtimeTranscriptionProvider(...) Provider Plugins
Realtime voice api.registerRealtimeVoiceProvider(...) Provider Plugins
Media understanding api.registerMediaUnderstandingProvider(...) Provider Plugins
Image generation api.registerImageGenerationProvider(...) Provider Plugins
Music generation api.registerMusicGenerationProvider(...) Provider Plugins
Video generation api.registerVideoGenerationProvider(...) Provider Plugins
Web fetch api.registerWebFetchProvider(...) Provider Plugins
Web search api.registerWebSearchProvider(...) Provider Plugins
Tool-result middleware api.registerAgentToolResultMiddleware(...) SDK Overview
Agent tools api.registerTool(...) Below
Custom commands api.registerCommand(...) Entry Points
Plugin hooks api.on(...) Plugin hooks
Internal event hooks api.registerHook(...) Entry Points
HTTP routes api.registerHttpRoute(...) Internals
CLI subcommands api.registerCli(...) Entry Points

For the full registration API, see SDK Overview.

Bundled plugins can use api.registerAgentToolResultMiddleware(...) when they need async tool-result rewriting before the model sees the output. Declare the targeted runtimes in contracts.agentToolResultMiddleware, for example ["pi", "codex"]. This is a trusted bundled-plugin seam; external plugins should prefer regular OpenClaw plugin hooks unless OpenClaw grows an explicit trust policy for this capability.

If your plugin registers custom gateway RPC methods, keep them on a plugin-specific prefix. Core admin namespaces (config.*, exec.approvals.*, wizard.*, update.*) stay reserved and always resolve to operator.admin, even if a plugin asks for a narrower scope.

openclaw/plugin-sdk/gateway-method-runtime is a reserved control-plane bridge for plugin HTTP routes that declare contracts.gatewayMethodDispatch: ["authenticated-request"]. It is an intentional-use guard for reviewed native plugins, not a sandbox boundary.

Hook guard semantics to keep in mind:

  • before_tool_call: { block: true } is terminal and stops lower-priority handlers.
  • before_tool_call: { block: false } is treated as no decision.
  • before_tool_call: { requireApproval: { ... } } pauses agent execution and prompts the user for approval via the exec approval overlay, native channel approval clients, or the /approve command on any channel.
  • before_install: { block: true } is terminal and stops lower-priority handlers.
  • before_install: { block: false } is treated as no decision.
  • message_sending: { cancel: true } is terminal and stops lower-priority handlers.
  • message_sending: { cancel: false } is treated as no decision.
  • message_received: prefer the typed threadId field when you need inbound thread/topic routing. Keep metadata for channel-specific extras.
  • message_sending: prefer typed replyToId / threadId routing fields over channel-specific metadata keys.

The /approve command handles both exec and plugin approvals with bounded fallback: when an exec approval id is not found, OpenClaw retries the same id through plugin approvals. Plugin approval forwarding can be configured independently via approvals.plugin in config.

If custom approval plumbing needs to detect that same bounded fallback case, prefer isApprovalNotFoundError from openclaw/plugin-sdk/error-runtime instead of matching approval-expiry strings manually.

See Plugin hooks for examples and the hook reference.

Registering agent tools

Tools are typed functions the LLM can call. They can be required (always available) or optional (user opt-in):

For simple plugins that only own a fixed set of tools, prefer defineToolPlugin. It generates manifest metadata and keeps contracts.tools aligned. Use the lower-level api.registerTool(...) surface when the plugin also owns channels, providers, hooks, services, commands, or fully dynamic tool registration.

register(api) {
  api.registerTool(
    {
      name: "workflow_tool",
      description: "Run a workflow",
      parameters: Type.Object({ pipeline: Type.String() }),
      async execute(_id, params) {
        return { content: [{ type: "text", text: params.pipeline }] };
      },
    },
    { optional: true },
  );
}

Every tool registered with api.registerTool(...) must also be declared in the plugin manifest:

{
  "contracts": {
    "tools": ["workflow_tool"]
  },
  "toolMetadata": {
    "workflow_tool": {
      "optional": true
    }
  }
}

Users opt in with tools.allow:

{
  tools: { allow: ["workflow_tool"] }, // or ["my-plugin"] for all tools from one plugin
}

Optional tools control whether a tool is exposed to the model. Use plugin permission requests when a tool or hook should ask for approval after the model selects it and before the action runs.

Use optional tools for side effects, unusual binaries, or capabilities that should not be exposed by default. Tool names must not conflict with core tools; conflicts are skipped and reported in plugin diagnostics. Malformed registrations, including tool descriptors without parameters, are skipped and reported the same way. Registered tools are typed functions the model can call after policy and allowlist checks pass.

Tool factories receive a runtime-supplied context object. Use ctx.activeModel when a tool needs to log, display, or adapt to the active model for the current turn. The object can include provider, modelId, and modelRef. Treat it as informational runtime metadata, not as a security boundary against the local operator, installed plugin code, or a modified OpenClaw runtime. Sensitive local tools should still require an explicit plugin or operator opt-in and fail closed when active-model metadata is missing or unsuitable.

The manifest declares ownership and discovery; execution still calls the live registered tool implementation. Keep toolMetadata.<tool>.optional: true aligned with api.registerTool(..., { optional: true }) so OpenClaw can avoid loading that plugin runtime until the tool is explicitly allowlisted.

Import conventions

Import from focused SDK subpaths:

import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";

Do not import from the deprecated root barrel:

import { definePluginEntry } from "openclaw/plugin-sdk";

Within your plugin package, use local barrel files such as api.ts and runtime-api.ts for internal imports. Do not import your own plugin through an SDK path. Provider-specific helpers should stay in the provider package unless the seam is truly generic.

Custom Gateway RPC methods are an advanced entry point. Keep them on a plugin-specific prefix; core admin namespaces such as config.*, exec.approvals.*, operator.admin.*, wizard.*, and update.* stay reserved and resolve to operator.admin. The openclaw/plugin-sdk/gateway-method-runtime bridge is reserved for plugin HTTP routes that declare contracts.gatewayMethodDispatch: ["authenticated-request"].

For the full import map, see Plugin SDK overview.

Pre-submission checklist

package.json has correct openclaw metadata openclaw.plugin.json manifest is present and valid Entry point uses defineChannelPluginEntry or definePluginEntry All imports use focused plugin-sdk/<subpath> paths Internal imports use local modules, not SDK self-imports Tests pass (pnpm test -- <bundled-plugin-root>/my-plugin/) pnpm check passes (in-repo plugins)

Test against beta releases

  1. Watch for GitHub release tags on openclaw/openclaw and subscribe via Watch > Releases. Beta tags look like v2026.3.N-beta.1. You can also turn on notifications for the official OpenClaw X account @openclaw for release announcements.
  2. Test your plugin against the beta tag as soon as it appears. The window before stable is typically only a few hours.
  3. Post in your plugin's thread in the plugin-forum Discord channel after testing with either all good or what broke. If you do not have a thread yet, create one.
  4. If something breaks, open or update an issue titled Beta blocker: <plugin-name> - <summary> and apply the beta-blocker label. Put the issue link in your thread.
  5. Open a PR to main titled fix(<plugin-id>): beta blocker - <summary> and link the issue in both the PR and your Discord thread. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation. Blockers with a PR get merged; blockers without one might ship anyway. Maintainers watch these threads during beta testing.
  6. Silence means green. If you miss the window, your fix likely lands in the next cycle.

Next steps

Build a messaging channel plugin Build a model provider plugin Register a local AI CLI backend Import map and registration API reference TTS, search, subagent via api.runtime Test utilities and patterns Full manifest schema reference