Building OpenClaw Plugins: A Quick Start Guide
Learn how to create your first OpenClaw plugin in minutes, covering requirements, plugin shapes, and installation via ClawHub. Ideal for developers extending OpenClaw with new capabilities.
Read this when
- 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 let you extend OpenClaw without touching its core. A plugin can bring in a messaging channel, a model provider, a local CLI backend, an agent tool, a hook, a media provider, or any other capability owned by a plugin.
There is no need to add an external plugin to the OpenClaw repository itself. Instead, publish the package to ClawHub and users can install it with:
openclaw plugins install clawhub:<package-name>
During the launch cutover, bare package specs still get installed from npm. When you want ClawHub resolution, apply the clawhub: prefix.
Requirements
- Node 22.22.3+, Node 24.15+, or Node 25.9+, plus
npmorpnpm. - TypeScript ESM modules.
- For bundled plugin work inside the repo, clone the repository and run
pnpm install. Developing plugins from a source checkout is pnpm-only, since OpenClaw finds bundled plugins fromextensions/*workspace packages.
Choose the plugin shape
-
Channel plugin, Link OpenClaw to a messaging platform.
-
Provider plugin, Add a model, media, search, fetch, speech, or realtime provider.
-
CLI backend plugin, Run a local AI CLI through OpenClaw model fallback.
-
Tool plugin, Register agent tools.
Quickstart
To build a minimal tool plugin, register one required agent tool. That shape is the shortest useful plugin and covers the package, manifest, entry point, and local proof.
Create package metadata
{
"name": "@myorg/openclaw-my-plugin",
"version": "1.0.0",
"type": "module",
"dependencies": {
"typebox": "1.1.39"
},
"peerDependencies": {
"openclaw": ">=2026.3.24-beta.2"
},
"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
}
}
For published external plugins, runtime entries should point at built JavaScript files. The full entry point contract is described in SDK entry points.
Every plugin needs a manifest, even when there is no config. Runtime tools must be listed in contracts.tools so OpenClaw can detect ownership without eagerly loading every plugin runtime. Set activation.onStartup deliberately; this example loads at Gateway startup.
Host-trusted plugin surfaces are also gated by the manifest and need explicit declaration for installed plugins: api.registerAgentToolResultMiddleware(...) requires each target runtime to be listed in contracts.agentToolResultMiddleware, and api.registerTrustedToolPolicy(...) requires each policy id to appear in contracts.trustedToolPolicies. These declarations keep install-time inspection and runtime registration in sync.
For a complete list of manifest fields, see Plugin manifest.
Register the tool
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() }),
outputSchema: Type.Object(
{ input: Type.String() },
{ additionalProperties: false },
),
async execute(_id, params) {
const details = { input: params.input };
return {
content: [{ type: "text", text: `Got: ${params.input}` }],
details,
};
},
});
},
});
For non-channel plugins, use definePluginEntry. Channel plugins should use defineChannelPluginEntry from openclaw/plugin-sdk/core instead.
Test the runtime
For an installed or external plugin, check the loaded runtime:
openclaw plugins inspect my-plugin --runtime --json
If the plugin registers a CLI command, run that command as well and verify the output, for instance 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:
pnpm test extensions/my-plugin/
pnpm check
Test the package install
Before publishing a package-ready plugin, test the same install shape users will get. Start by adding a build step, pointing runtime entries such as openclaw.extensions at built JavaScript like ./dist/index.js, and ensuring npm pack includes that dist/ output. TypeScript source entries are only for source checkouts and local development paths.
Then pack the plugin and install the tarball with npm-pack::
npm pack --pack-destination /tmp
openclaw plugins install npm-pack:/tmp/<plugin-package>.tgz --force
openclaw plugins inspect my-plugin --runtime --json
npm-pack: uses OpenClaw's managed per-plugin npm project, so it catches runtime dependency mistakes that source checkout testing can hide. It proves the package and dependency shape, not catalog-linked official trust.
Runtime imports must go in dependencies or optionalDependencies; dependencies left only in devDependencies will not be installed for the managed runtime project.
Do not rely on a raw archive or path install as the final proof for official or privileged plugin behavior. Raw sources are fine for local debugging, but they do not prove the same dependency path as npm or ClawHub installs. If your plugin depends on trusted official plugin status, add a second proof through a catalog-backed official install or a published package path that records official trust. See Plugin dependency resolution for install-root and dependency ownership details.
Publish
Validate the package before publishing:
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
Canonical ClawHub package snippets live in docs/snippets/plugin-publish/.
Install
Install the published package through ClawHub:
openclaw plugins install clawhub:your-org/your-plugin
Registering tools
Tools can be required or optional. Required tools are always available when the plugin is enabled. Optional tools need explicit user opt-in before OpenClaw loads the owning plugin runtime.
Tool factories receive trusted runtime context, including deliveryContext, nativeChannelId for the active platform conversation when available, and requesterSenderId. A factory can use toolContext.delivery?.send({ text, mediaUrl }) to send text or media to the current conversation. The property is unavailable outside an active channel turn or when the channel uses Gateway-owned delivery. OpenClaw binds the route, account, thread, and media access policy; the capability expires when the turn ends.
register(api) {
api.registerTool(
(toolContext) => ({
name: "workflow_tool",
description: "Run a workflow",
parameters: Type.Object({ pipeline: Type.String() }),
outputSchema: Type.Object(
{ pipeline: Type.String() },
{ additionalProperties: false },
),
async execute(_id, params) {
await toolContext.delivery?.send({
text: `Workflow started: ${params.pipeline}`,
});
return {
content: [{ type: "text", text: params.pipeline }],
details: { pipeline: params.pipeline },
};
},
}),
{ name: "workflow_tool", optional: true },
);
}
outputSchema is not required. It defines the structured details value that Code Mode and Tool Search rely on. Before execution, catalog calls reject schemas that are invalid, and after tool hooks run, the final value gets validated. Tools that do not produce a stable JSON result can leave this out. The complete contract is documented in Tool plugins.
Any tool registered through api.registerTool(...) has to appear in the plugin manifest as well:
{
"contracts": {
"tools": ["workflow_tool"]
},
"toolMetadata": {
"workflow_tool": {
"optional": true
}
}
}
Users enable this with tools.allow:
{
tools: { allow: ["workflow_tool"] }, // or ["my-plugin"] for every tool from one plugin
}
Optional tools decide whether the model can see a given tool. When a tool or hook needs approval after the model picks it but before the action executes, use plugin permission requests.
With toolMetadata.<tool>.profiles, a plugin tool gets added to the allowlists of named built-in profiles. "profiles": ["coding", "messaging"], for instance, makes it visible in those profiles without creating a core catalog entry. Explicit operator allowlists and deny rules still take precedence.
Side effects, unusual binaries, or capabilities that should stay hidden by default are good candidates for optional tools. Tool names must not clash with core tool names; when they do, the conflict is skipped and flagged in plugin diagnostics. Registrations that are malformed get skipped and reported the same way: a missing non-empty name, a execute that is not a function, or a tool descriptor lacking a parameters object.
Tool factories get a context object supplied by the runtime. When a tool must log, display, or adjust to the active model for the current turn, ctx.activeModel is the way to do it; it may carry provider, modelId, and modelRef. Treat this 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 demand an explicit plugin or operator opt-in and fail closed when active-model metadata is missing or unsuitable.
Ownership and discovery are declared in the manifest, but execution still invokes the live registered tool implementation. Keep toolMetadata.<tool>.optional: true in line with api.registerTool(..., { optional: true }) so OpenClaw can skip 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";
Inside your plugin package, rely on 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 belong in the provider package unless the seam is genuinely generic.
Custom Gateway RPC methods serve as an advanced entry point. Put them on a plugin-specific prefix; core admin namespaces like config.*, exec.approvals.*, operator.admin.*, wizard.*, and update.* remain reserved and resolve to operator.admin. The openclaw/plugin-sdk/gateway-method-runtime bridge is set aside for plugin HTTP routes that declare contracts.gatewayMethodDispatch: ["authenticated-request"].
The full import map lives in Plugin SDK overview.
OpenClaw SDK compatibility fields carry TypeScript @deprecated annotations, which editors surface as migration warnings. To enforce them at build time, enable a type-aware rule such as @typescript-eslint/no-deprecated. Oxlint is not type-aware, so it cannot enforce these annotations.
Pre-submission checklist
Check
package.json has correct
openclawmetadata
Check
openclaw.plugin.json manifest is present and valid
Check
Entry point uses
defineChannelPluginEntryordefinePluginEntry
Check
All imports use focused
plugin-sdk/<subpath>paths
Check
Internal imports use local modules, not SDK self-imports
Check
Tests pass (
pnpm test <bundled-plugin-root>/my-plugin/)
Check
pnpm checkpasses (in-repo plugins)
Test against beta releases
- Keep an eye on openclaw/openclaw releases (
Watch>Releases). Beta tags look likev2026.3.N-beta.1. Release announcements also go out via @openclaw on X. - As soon as the beta tag appears, test your plugin against it. The gap before stable is usually just a few hours.
- After testing, post in your plugin's thread in the
plugin-forumDiscord channel (discord.gg/clawd), noting eitherall goodor what broke. If you do not have a thread yet, create one. - When something breaks, open or update an issue titled
Beta blocker: <plugin-name> - <summary>and add thebeta-blockerlabel. Link the issue in your thread. - Open a PR to
maintitledfix(<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. - Silence means green. Missing the window usually means your fix lands in the next cycle.
Next steps
-
Channel Plugins, Build a messaging channel plugin
-
Provider Plugins, Build a model provider plugin
-
CLI Backend Plugins, Set up a local AI CLI backend through registration
-
SDK Overview, Reference for the import map and registration API
-
Runtime Helpers, Access TTS, search, and subagent features via api.runtime
-
Testing, Utilities and patterns for running tests
-
Plugin Manifest, Complete schema reference for the manifest