docs: rename Extensions to Plugins, rewrite building guide as capability-agnostic, move voice-call to Channels

This commit is contained in:
Vincent Koc 2026-03-20 10:44:11 -07:00
parent 1cabb053ad
commit ad4536fd7e
3 changed files with 178 additions and 94 deletions

View File

@ -948,6 +948,7 @@
"channels/telegram",
"channels/tlon",
"channels/twitch",
"plugins/voice-call",
"channels/whatsapp",
"channels/zalo",
"channels/zalouser"
@ -1073,15 +1074,13 @@
]
},
{
"group": "Extensions",
"group": "Plugins",
"pages": [
"plugins/building-extensions",
"plugins/sdk-migration",
"plugins/architecture",
"plugins/community",
"plugins/bundles",
"plugins/voice-call",
"plugins/zalouser",
"plugins/manifest",
"plugins/agent-tools",
"tools/capability-cookbook"

View File

@ -1,79 +1,116 @@
---
title: "Building Extensions"
summary: "Step-by-step guide for creating OpenClaw channel and provider extensions"
title: "Building Plugins"
sidebarTitle: "Building Plugins"
summary: "Step-by-step guide for creating OpenClaw plugins with any combination of capabilities"
read_when:
- You want to create a new OpenClaw plugin or extension
- You want to create a new OpenClaw plugin
- You need to understand the plugin SDK import patterns
- You are adding a new channel or provider to OpenClaw
- You are adding a new channel, provider, tool, or other capability to OpenClaw
---
# Building Extensions
# Building Plugins
Extensions add channels, model providers, tools, or other capabilities to OpenClaw.
This guide walks through creating one from scratch.
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
- OpenClaw repository cloned and dependencies installed (`pnpm install`)
- Node >= 22 and a package manager (npm or pnpm)
- Familiarity with TypeScript (ESM)
- For in-repo plugins: OpenClaw repository cloned and `pnpm install` done
## Extension structure
## Plugin capabilities
Every extension lives under `extensions/<name>/` and follows this layout:
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):
```
extensions/my-channel/
my-plugin/
├── package.json # npm metadata + openclaw config
├── index.ts # Entry point (defineChannelPluginEntry)
├── openclaw.plugin.json # Plugin manifest
├── index.ts # Entry point
├── setup-entry.ts # Setup wizard (optional)
├── api.ts # Public contract barrel (optional)
├── runtime-api.ts # Internal runtime barrel (optional)
├── api.ts # Public exports (optional)
├── runtime-api.ts # Internal exports (optional)
└── src/
├── channel.ts # Channel adapter implementation
├── provider.ts # Capability implementation
├── runtime.ts # Runtime wiring
└── *.test.ts # Colocated tests
```
## Create an extension
## Create a plugin
<Steps>
<Step title="Create the package">
Create `extensions/my-channel/package.json`:
Create `package.json` with the `openclaw` metadata block. The structure
depends on what capabilities your plugin provides.
**Channel plugin example:**
```json
{
"name": "@openclaw/my-channel",
"version": "2026.1.1",
"description": "OpenClaw My Channel plugin",
"name": "@myorg/openclaw-my-channel",
"version": "1.0.0",
"type": "module",
"dependencies": {},
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "my-channel",
"label": "My Channel",
"selectionLabel": "My Channel (plugin)",
"docsPath": "/channels/my-channel",
"docsLabel": "my-channel",
"blurb": "Short description of the channel.",
"order": 80
},
"install": {
"npmSpec": "@openclaw/my-channel",
"localPath": "extensions/my-channel"
"blurb": "Short description of the channel."
}
}
}
```
The `openclaw` field tells the plugin system what your extension provides.
For provider plugins, use `providers` instead of `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.
</Step>
<Step title="Define the entry point">
Create `extensions/my-channel/index.ts`:
The entry point registers your capabilities with the plugin API.
**Channel plugin:**
```typescript
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
@ -88,23 +125,51 @@ extensions/my-channel/
});
```
For provider plugins, use `definePluginEntry` instead.
**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.
</Step>
<Step title="Import from focused subpaths">
Always import from specific `openclaw/plugin-sdk/<subpath>` paths rather than
the monolithic root. The old `openclaw/plugin-sdk/compat` barrel is deprecated
(see [SDK Migration](/plugins/sdk-migration)).
<Step title="Import from focused SDK subpaths">
Always import from specific `openclaw/plugin-sdk/\<subpath\>` paths. The old
monolithic import is deprecated (see [SDK Migration](/plugins/sdk-migration)).
```typescript
// Correct: focused subpaths
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth";
// Wrong: monolithic root (lint will reject this)
@ -114,10 +179,10 @@ extensions/my-channel/
<Accordion title="Common subpaths reference">
| Subpath | Purpose |
| --- | --- |
| `plugin-sdk/core` | Plugin entry definitions, base types |
| `plugin-sdk/channel-setup` | Optional setup adapters/wizards |
| `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` | Prefix + typing reply wiring |
| `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 |
@ -130,95 +195,115 @@ extensions/my-channel/
| `plugin-sdk/testing` | Test utilities |
</Accordion>
Use the narrowest primitive that matches the job. Reach for `channel-runtime`
or other larger helper barrels only when a dedicated subpath does not exist yet.
Use the narrowest subpath that matches the job.
</Step>
<Step title="Use local barrels for internal imports">
Within your extension, create barrel files for internal code sharing instead
of importing through the plugin SDK:
<Step title="Use local modules for internal imports">
Within your plugin, create local module files for internal code sharing
instead of re-importing through the plugin SDK:
```typescript
// api.ts — public contract for this extension
export { MyChannelConfig } from "./src/config.js";
export { MyChannelRuntime } from "./src/runtime.js";
// 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 (not for production consumers)
// runtime-api.ts — internal-only exports
export { internalHelper } from "./src/helpers.js";
```
<Warning>
Never import your own extension back through its published SDK contract
path from production files. Route internal imports through `./api.ts` or
`./runtime-api.ts` instead. The SDK contract is for external consumers only.
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>
</Step>
<Step title="Add a plugin manifest">
Create `openclaw.plugin.json` in your extension root:
Create `openclaw.plugin.json` in your plugin root:
```json
{
"id": "my-channel",
"kind": "channel",
"channels": ["my-channel"],
"name": "My Channel Plugin",
"description": "Connects OpenClaw to My Channel"
"id": "my-plugin",
"kind": "provider",
"name": "My Plugin",
"description": "Adds My Provider to OpenClaw"
}
```
See [Plugin manifest](/plugins/manifest) for the full schema.
For channel plugins, set `"kind": "channel"` and add `"channels": ["my-channel"]`.
See [Plugin Manifest](/plugins/manifest) for the full schema.
</Step>
<Step title="Test with contract tests">
OpenClaw runs contract tests against all registered plugins. After adding your
extension, run:
<Step title="Test your plugin">
**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
```
Contract tests verify your plugin conforms to the expected interface (setup
wizard, session binding, message handling, group policy, etc.).
For unit tests, import test helpers from the public testing surface:
For unit tests, import test helpers from the testing surface:
```typescript
import { createTestRuntime } from "openclaw/plugin-sdk/testing";
```
</Step>
<Step title="Publish and install">
**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>
```
</Step>
</Steps>
## Lint enforcement
## Lint enforcement (in-repo plugins)
Three scripts enforce SDK boundaries:
Three scripts enforce SDK boundaries for plugins in the OpenClaw repository:
1. **No monolithic root imports**`openclaw/plugin-sdk` root is rejected
2. **No direct src/ imports** — extensions cannot import `../../src/` directly
3. **No self-imports** — extensions cannot import their own `plugin-sdk/<name>` subpath
2. **No direct src/ imports**plugins cannot import `../../src/` directly
3. **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
<Check>**package.json** has correct `openclaw` metadata</Check>
<Check>Entry point uses `defineChannelPluginEntry` or `definePluginEntry`</Check>
<Check>All imports use focused `plugin-sdk/<subpath>` paths</Check>
<Check>Internal imports use local barrels, not SDK self-imports</Check>
<Check>All imports use focused `plugin-sdk/\<subpath\>` paths</Check>
<Check>Internal imports use local modules, not SDK self-imports</Check>
<Check>`openclaw.plugin.json` manifest is present and valid</Check>
<Check>Contract tests pass (`pnpm test:contracts`)</Check>
<Check>Unit tests colocated as `*.test.ts`</Check>
<Check>`pnpm check` passes (lint + format)</Check>
<Check>Doc page created under `docs/channels/` or `docs/plugins/`</Check>
<Check>Tests pass</Check>
<Check>`pnpm check` passes (in-repo plugins)</Check>
## Related
- [Plugin SDK Migration](/plugins/sdk-migration) — migrating from compat to focused subpaths
- [Plugin SDK Migration](/plugins/sdk-migration) — migrating from the deprecated compat import
- [Plugin Architecture](/plugins/architecture) — internals and capability model
- [Plugin Manifest](/plugins/manifest) — full manifest schema
- [Community Plugins](/plugins/community) — existing community extensions
- [Plugin Agent Tools](/plugins/agent-tools) — adding agent tools in a plugin
- [Community Plugins](/plugins/community) — listing and quality bar

View File

@ -16,7 +16,7 @@ what changed, why, and how to migrate.
## Why this change
The monolithic compat barrel re-exported everything from a single entry point.
The monolithic compat entry re-exported everything from a single entry point.
This caused:
- **Slow startup**: importing one helper pulled in dozens of unrelated modules.
@ -28,14 +28,14 @@ with a clear purpose.
## What triggers the warning
If your plugin imports from the compat barrel, you will see:
If your plugin imports from the compat entry, you will see:
```
[OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED] Warning: openclaw/plugin-sdk/compat is
deprecated for new plugins. Migrate to focused openclaw/plugin-sdk/\<subpath\> imports.
```
The compat barrel still works at runtime. This is a deprecation warning, not an
The compat entry still works at runtime. This is a deprecation warning, not an
error. But new plugins **must not** use it, and existing plugins should migrate
before compat is removed.
@ -54,7 +54,7 @@ grep -r "plugin-sdk/compat" extensions/my-plugin/
Each export from compat maps to a specific subpath. Replace the import source:
```typescript
// Before (compat barrel)
// Before (compat entry)
import {
createChannelReplyPipeline,
createPluginRuntimeStore,
@ -106,8 +106,8 @@ check the source at `src/plugin-sdk/` or ask in Discord.
## Compat barrel removal timeline
- **Now**: compat barrel emits a deprecation warning at runtime.
- **Next major release**: compat barrel will be removed. Plugins still using it will
- **Now**: compat entry emits a deprecation warning at runtime.
- **Next major release**: compat entry will be removed. Plugins still using it will
fail to import.
Bundled plugins (under `extensions/`) have already been migrated. External plugins