docs/plugins/building-plugins.md
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.
npm or pnpm.pnpm install.
Source-checkout plugin development is pnpm-only because OpenClaw loads bundled
plugins from extensions/* workspace packages.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.
<Steps> <Step title="Create package metadata"> <CodeGroup>{
"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).
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`.
```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
```
```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/`.
```bash
openclaw plugins install clawhub:your-org/your-plugin
```
<a id="registering-agent-tools"></a>
Tools can be required or optional. Required tools are always available when the plugin is enabled. Optional tools require user opt-in.
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
}
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 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.
<Check>package.json has correct openclaw metadata</Check>
<Check>openclaw.plugin.json manifest is present and valid</Check>
<Check>Entry point uses defineChannelPluginEntry or definePluginEntry</Check>
<Check>All imports use focused plugin-sdk/<subpath> paths</Check>
<Check>Internal imports use local modules, not SDK self-imports</Check>
<Check>Tests pass (pnpm test -- <bundled-plugin-root>/my-plugin/)</Check>
<Check>pnpm check passes (in-repo plugins)</Check>
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.plugin-forum Discord channel after testing with either all good or what broke. If you do not have a thread yet, create one.Beta blocker: <plugin-name> - <summary> and apply the beta-blocker label. Put the issue link in your thread.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.