* chore(deps): refresh eligible seven-day npm dependencies * docs(plugins): align embedded TypeBox dependency pins * test(deps): align evidence and Escape ownership * fix(ci): repair native PID imports and cancellation assertions * test(ui): make effort Escape ownership explicit * fix(agents): keep error presentation on prepared policy * fix(agents): preserve loaded provider policy in error presentation * fix(agents): carry prepared provider owners into lifecycle errors Preserve endpoint-owned recovery guidance for custom provider routes in terminal events and callbacks. Reuse the prepared model handle and full-signal classifier, with a real Agent/AgentSession boundary regression. * fix(agents): reconcile explicit diagnostic ownership and structured errors Keep presentation on explicit prepared owners, preserve full assistant error facts ahead of generic request wrappers, and retain raw-schema diagnostics. Carry prepared owners into terminal observations and prove source/compiled scope boundaries. Complete the shared attempt fixture with the real model-handle getter. * fix(agents): carry full classified facts into safe failure copy Share explicit-owner assistant classification between direct formatting and the user-facing wrapper. Preserve structured codes, types and body evidence in safe provider/model/status copy, including message-less failures, while retaining raw-schema diagnostics and ownerless policy boundaries. * fix(ui): keep Home work context lazy and current Let the existing deferred assistant panel prepare page work context once, using the shell's validated route facts. Keep explicit agent ownership through global/main aliases and refresh the quoted reference when session, agent or Gateway snapshots change. Reuse the frozen refinement from PR #134059: 6e2a8f9550e6e6da957b0352fa674024f198118e. Add source-bound roster-refresh/send proof, extend the existing owner fixture for snapshot updates and cleanup, and regenerate the boot manifest for the pinned dependency graph. Startup gzip is 347243 B under the unchanged 347353 B gate. The UI repair removes two production lines net. * test(agents): align generation fixtures with prepared metadata Use the captured main generation-scope contract in lifecycle and source/compiled provider-owner fixtures. Remove its retired config input while preserving provider selection and empty-generation fencing. Integrate captured main10564e2with the Home context refinement from PR #134059 and the assistant dock cleanup from PR #134435. Preserve the existing contributor credit and canonical catalog owner already on main. The integrated candidate passes the normal full build, scoped checks, 679 original-order model cases, 400 backend owner cases, both catalog E2Es, 290 UI cases and 18 browser cases. Final grouped startup gzip is 347299 B under the unchanged 347353 B enforcement limit. * refactor(ui): keep submission projection in lazy chat owner Keep the app store responsible for bounded retained bytes and client lifetime. Move receipt adaptation and display retirement into the existing history projection owner, shared by both lazy chat consumers. Preserve missing-store behavior and the retained-prompt, attachment and reconnect contracts. Continue the retained-submission owner from PR #134059 (0f3e17e56b). Validation: 808 owner tests, 21 Chromium cases, grouped-bundle Home and retired-prompt proof, changed checks and fresh full-candidate autoreview. Startup gzip: 347588 -> 347320 bytes; unchanged limit 347353.
15 KiB
| summary | title | sidebarTitle | doc-schema-version | read_when | |||
|---|---|---|---|---|---|---|---|
| Create your first OpenClaw plugin in minutes | Building plugins | Getting Started | 1 |
|
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
- Node 22.22.3+, Node 24.15+, or Node 25.9+, and
npmorpnpm. - 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 discovers bundled plugins fromextensions/*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 covers the package, manifest, entry point, and local proof.
{
"name": "@myorg/openclaw-my-plugin",
"version": "1.0.0",
"type": "module",
"dependencies": {
"typebox": "1.3.17"
},
"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
}
}
</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 with 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 loads on Gateway startup.
Host-trusted plugin surfaces are manifest-gated too and require explicit
declaration for installed plugins: `api.registerAgentToolResultMiddleware(...)`
needs each target runtime listed in `contracts.agentToolResultMiddleware`,
and `api.registerTrustedToolPolicy(...)` needs each policy id in
`contracts.trustedToolPolicies`. These declarations keep install-time
inspection and runtime registration aligned.
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() }),
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,
};
},
});
},
});
```
Use `definePluginEntry` for non-channel plugins. Channel plugins use
`defineChannelPluginEntry` from `openclaw/plugin-sdk/core` instead.
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 and confirm
output, for example `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
```
Before publishing a package-ready plugin, test the same install shape users
will get. First add a build step, point runtime entries such as
`openclaw.extensions` at built JavaScript like `./dist/index.js`, and make
sure `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:`:
```bash
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 be in `dependencies` or `optionalDependencies`;
dependencies left only in `devDependencies` will not be installed for the
managed runtime project.
Do not use a raw archive/path install as the final proof for official or
privileged plugin behavior. Raw sources are useful for local debugging, but
they do not prove the same dependency path as npm or ClawHub installs. If
your plugin relies 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](/plugins/dependency-resolution) for
install-root and dependency ownership details.
Validate the package before publishing:
```bash
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 the published package through ClawHub:
```bash
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 optional. It describes the structured details value used by
Code Mode and Tool Search. Catalog
calls reject invalid schemas before execution and validate the final value after
tool hooks. Omit it for tools without a stable JSON result. See
Tool plugins for the full contract.
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 every tool 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.
toolMetadata.<tool>.profiles adds a plugin tool to named built-in profile
allowlists. For example, "profiles": ["coding", "messaging"] exposes it in
those profiles without adding a core catalog entry. Explicit operator
allowlists and deny rules remain authoritative.
Use optional tools for side effects, unusual binaries, or capabilities that
should not be exposed by default. Tool names must not conflict with core tool
names; conflicts are skipped and reported in plugin diagnostics. Malformed
registrations are skipped and reported the same way: a missing non-empty
name, a non-function execute, or a tool descriptor without a parameters
object.
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; it can include provider, modelId, and modelRef. Treat it as
informational runtime metadata, not 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";
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.
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
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
- Watch openclaw/openclaw releases (
Watch>Releases). Beta tags look likev2026.3.N-beta.1. You can also follow @openclaw on X for release announcements. - Test your plugin against the beta tag as soon as it appears. The window before stable is typically only a few hours.
- Post in your plugin's thread in the
plugin-forumDiscord channel (discord.gg/clawd) after testing, with eitherall goodor what broke. Create a thread if you do not have one yet. - If something breaks, open or update an issue titled
Beta blocker: <plugin-name> - <summary>and apply 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.