11 KiB
title, sidebarTitle, summary, read_when
| title | sidebarTitle | summary | read_when | |||
|---|---|---|---|---|---|---|
| Building Plugins | Building Plugins | Step-by-step guide for creating OpenClaw plugins with any combination of capabilities |
|
Building Plugins
Plugins extend OpenClaw with new capabilities: channels, model providers, speech, image generation, web search, agent tools, or any combination. A single plugin can register multiple capabilities.
OpenClaw encourages external plugin development. You do not need to add your
plugin to the OpenClaw repository. Publish your plugin on npm, and users install
it with openclaw plugins install <npm-spec>. OpenClaw also maintains a set of
core plugins in-repo, but the plugin system is designed for independent ownership
and distribution.
Prerequisites
- Node >= 22 and a package manager (npm or pnpm)
- Familiarity with TypeScript (ESM)
- For in-repo plugins: OpenClaw repository cloned and
pnpm installdone
Plugin capabilities
A plugin can register one or more capabilities. The capability you register determines what your plugin provides to OpenClaw:
| Capability | Registration method | What it adds |
|---|---|---|
| Text inference | api.registerProvider(...) |
Model provider (LLM) |
| Channel / messaging | api.registerChannel(...) |
Chat channel (e.g. Slack, IRC) |
| Speech | api.registerSpeechProvider(...) |
Text-to-speech / STT |
| Media understanding | api.registerMediaUnderstandingProvider(...) |
Image/audio/video analysis |
| Image generation | api.registerImageGenerationProvider(...) |
Image generation |
| Web search | api.registerWebSearchProvider(...) |
Web search provider |
| Agent tools | api.registerTool(...) |
Tools callable by the agent |
A plugin that registers zero capabilities but provides hooks or services is a hook-only plugin. That pattern is still supported.
Plugin structure
Plugins follow this layout (whether in-repo or standalone):
my-plugin/
├── package.json # npm metadata + openclaw config
├── openclaw.plugin.json # Plugin manifest
├── index.ts # Entry point
├── setup-entry.ts # Setup wizard (optional)
├── api.ts # Public exports (optional)
├── runtime-api.ts # Internal exports (optional)
└── src/
├── provider.ts # Capability implementation
├── runtime.ts # Runtime wiring
└── *.test.ts # Colocated tests
Create a plugin
Create `package.json` with the `openclaw` metadata block. The structure depends on what capabilities your plugin provides.**Channel plugin example:**
```json
{
"name": "@myorg/openclaw-my-channel",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"channel": {
"id": "my-channel",
"label": "My Channel",
"blurb": "Short description of the channel."
}
}
}
```
**Provider plugin example:**
```json
{
"name": "@myorg/openclaw-my-provider",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"providers": ["my-provider"]
}
}
```
The `openclaw` field tells the plugin system what your plugin provides.
A plugin can declare both `channel` and `providers` if it provides multiple
capabilities.
The entry point registers your capabilities with the plugin API.
**Channel plugin:**
```typescript
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
export default defineChannelPluginEntry({
id: "my-channel",
name: "My Channel",
description: "Connects OpenClaw to My Channel",
plugin: {
// Channel adapter implementation
},
});
```
**Provider plugin:**
```typescript
import { definePluginEntry } from "openclaw/plugin-sdk/core";
export default definePluginEntry({
id: "my-provider",
name: "My Provider",
register(api) {
api.registerProvider({
// Provider implementation
});
},
});
```
**Multi-capability plugin** (provider + tool):
```typescript
import { definePluginEntry } from "openclaw/plugin-sdk/core";
export default definePluginEntry({
id: "my-plugin",
name: "My Plugin",
register(api) {
api.registerProvider({ /* ... */ });
api.registerTool({ /* ... */ });
api.registerImageGenerationProvider({ /* ... */ });
},
});
```
Use `defineChannelPluginEntry` for channel plugins and `definePluginEntry`
for everything else. A single plugin can register as many capabilities as needed.
Always import from specific `openclaw/plugin-sdk/\` paths. The old
monolithic import is deprecated (see [SDK Migration](/plugins/sdk-migration)).
If older plugin code still imports `openclaw/extension-api`, treat that as a
temporary compatibility bridge only. New code should use injected runtime
helpers such as `api.runtime.agent.*` instead of importing host-side agent
helpers directly.
```typescript
// Correct: focused subpaths
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth";
// Wrong: monolithic root (lint will reject this)
import { ... } from "openclaw/plugin-sdk";
// Deprecated: legacy host bridge
import { runEmbeddedPiAgent } from "openclaw/extension-api";
```
<Accordion title="Common subpaths reference">
| Subpath | Purpose |
| --- | --- |
| `plugin-sdk/core` | Plugin entry definitions and base types |
| `plugin-sdk/channel-setup` | Setup wizard adapters |
| `plugin-sdk/channel-pairing` | DM pairing primitives |
| `plugin-sdk/channel-reply-pipeline` | Reply prefix + typing wiring |
| `plugin-sdk/channel-config-schema` | Config schema builders |
| `plugin-sdk/channel-policy` | Group/DM policy helpers |
| `plugin-sdk/secret-input` | Secret input parsing/helpers |
| `plugin-sdk/webhook-ingress` | Webhook request/target helpers |
| `plugin-sdk/runtime-store` | Persistent plugin storage |
| `plugin-sdk/allow-from` | Allowlist resolution |
| `plugin-sdk/reply-payload` | Message reply types |
| `plugin-sdk/provider-oauth` | OAuth login + PKCE helpers |
| `plugin-sdk/provider-onboard` | Provider onboarding config patches |
| `plugin-sdk/testing` | Test utilities |
</Accordion>
Use the narrowest subpath that matches the job.
Within your plugin, create local module files for internal code sharing
instead of re-importing through the plugin SDK:
```typescript
// api.ts — public exports for this plugin
export { MyConfig } from "./src/config.js";
export { MyRuntime } from "./src/runtime.js";
// runtime-api.ts — internal-only exports
export { internalHelper } from "./src/helpers.js";
```
<Warning>
Never import your own plugin back through its published SDK path from
production files. Route internal imports through local files like `./api.ts`
or `./runtime-api.ts`. The SDK path is for external consumers only.
</Warning>
Create `openclaw.plugin.json` in your plugin root:
```json
{
"id": "my-plugin",
"kind": "provider",
"name": "My Plugin",
"description": "Adds My Provider to OpenClaw"
}
```
For channel plugins, set `"kind": "channel"` and add `"channels": ["my-channel"]`.
See [Plugin Manifest](/plugins/manifest) for the full schema.
**External plugins:** run your own test suite against the plugin SDK contracts.
**In-repo plugins:** OpenClaw runs contract tests against all registered plugins:
```bash
pnpm test:contracts:channels # channel plugins
pnpm test:contracts:plugins # provider plugins
```
For unit tests, import test helpers from the testing surface:
```typescript
import { createTestRuntime } from "openclaw/plugin-sdk/testing";
```
**External plugins:** publish to npm, then install:
```bash
npm publish
openclaw plugins install @myorg/openclaw-my-plugin
```
**In-repo plugins:** place the plugin under `extensions/` and it is
automatically discovered during build.
Users can browse and install community plugins with:
```bash
openclaw plugins search <query>
openclaw plugins install <npm-spec>
```
Lint enforcement (in-repo plugins)
Three scripts enforce SDK boundaries for plugins in the OpenClaw repository:
- No monolithic root imports —
openclaw/plugin-sdkroot is rejected - No direct src/ imports — plugins cannot import
../../src/directly - No self-imports — plugins cannot import their own
plugin-sdk/\<name\>subpath
Run pnpm check to verify all boundaries before committing.
External plugins are not subject to these lint rules, but following the same patterns is strongly recommended.
Pre-submission checklist
package.json has correct openclaw metadata
Entry point uses defineChannelPluginEntry or definePluginEntry
All imports use focused plugin-sdk/\<subpath\> paths
Internal imports use local modules, not SDK self-imports
openclaw.plugin.json manifest is present and valid
Tests pass
pnpm check passes (in-repo plugins)
Related
- Plugin SDK Migration — migrating from deprecated compat surfaces
- Plugin Architecture — internals and capability model
- Plugin Manifest — full manifest schema
- Plugin Agent Tools — adding agent tools in a plugin
- Community Plugins — listing and quality bar