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

View File

@ -1,79 +1,116 @@
--- ---
title: "Building Extensions" title: "Building Plugins"
summary: "Step-by-step guide for creating OpenClaw channel and provider extensions" sidebarTitle: "Building Plugins"
summary: "Step-by-step guide for creating OpenClaw plugins with any combination of capabilities"
read_when: 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 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. Plugins extend OpenClaw with new capabilities: channels, model providers, speech,
This guide walks through creating one from scratch. 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 ## Prerequisites
- OpenClaw repository cloned and dependencies installed (`pnpm install`) - Node >= 22 and a package manager (npm or pnpm)
- Familiarity with TypeScript (ESM) - 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 ├── 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) ├── setup-entry.ts # Setup wizard (optional)
├── api.ts # Public contract barrel (optional) ├── api.ts # Public exports (optional)
├── runtime-api.ts # Internal runtime barrel (optional) ├── runtime-api.ts # Internal exports (optional)
└── src/ └── src/
├── channel.ts # Channel adapter implementation ├── provider.ts # Capability implementation
├── runtime.ts # Runtime wiring ├── runtime.ts # Runtime wiring
└── *.test.ts # Colocated tests └── *.test.ts # Colocated tests
``` ```
## Create an extension ## Create a plugin
<Steps> <Steps>
<Step title="Create the package"> <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 ```json
{ {
"name": "@openclaw/my-channel", "name": "@myorg/openclaw-my-channel",
"version": "2026.1.1", "version": "1.0.0",
"description": "OpenClaw My Channel plugin",
"type": "module", "type": "module",
"dependencies": {},
"openclaw": { "openclaw": {
"extensions": ["./index.ts"], "extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"channel": { "channel": {
"id": "my-channel", "id": "my-channel",
"label": "My Channel", "label": "My Channel",
"selectionLabel": "My Channel (plugin)", "blurb": "Short description of the channel."
"docsPath": "/channels/my-channel",
"docsLabel": "my-channel",
"blurb": "Short description of the channel.",
"order": 80
},
"install": {
"npmSpec": "@openclaw/my-channel",
"localPath": "extensions/my-channel"
} }
} }
} }
``` ```
The `openclaw` field tells the plugin system what your extension provides. **Provider plugin example:**
For provider plugins, use `providers` instead of `channel`.
```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>
<Step title="Define the entry point"> <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 ```typescript
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; 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>
<Step title="Import from focused subpaths"> <Step title="Import from focused SDK subpaths">
Always import from specific `openclaw/plugin-sdk/<subpath>` paths rather than Always import from specific `openclaw/plugin-sdk/\<subpath\>` paths. The old
the monolithic root. The old `openclaw/plugin-sdk/compat` barrel is deprecated monolithic import is deprecated (see [SDK Migration](/plugins/sdk-migration)).
(see [SDK Migration](/plugins/sdk-migration)).
```typescript ```typescript
// Correct: focused subpaths // Correct: focused subpaths
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core"; import { definePluginEntry } from "openclaw/plugin-sdk/core";
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; 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"; import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth";
// Wrong: monolithic root (lint will reject this) // Wrong: monolithic root (lint will reject this)
@ -114,10 +179,10 @@ extensions/my-channel/
<Accordion title="Common subpaths reference"> <Accordion title="Common subpaths reference">
| Subpath | Purpose | | Subpath | Purpose |
| --- | --- | | --- | --- |
| `plugin-sdk/core` | Plugin entry definitions, base types | | `plugin-sdk/core` | Plugin entry definitions and base types |
| `plugin-sdk/channel-setup` | Optional setup adapters/wizards | | `plugin-sdk/channel-setup` | Setup wizard adapters |
| `plugin-sdk/channel-pairing` | DM pairing primitives | | `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-config-schema` | Config schema builders |
| `plugin-sdk/channel-policy` | Group/DM policy helpers | | `plugin-sdk/channel-policy` | Group/DM policy helpers |
| `plugin-sdk/secret-input` | Secret input parsing/helpers | | `plugin-sdk/secret-input` | Secret input parsing/helpers |
@ -130,95 +195,115 @@ extensions/my-channel/
| `plugin-sdk/testing` | Test utilities | | `plugin-sdk/testing` | Test utilities |
</Accordion> </Accordion>
Use the narrowest primitive that matches the job. Reach for `channel-runtime` Use the narrowest subpath that matches the job.
or other larger helper barrels only when a dedicated subpath does not exist yet.
</Step> </Step>
<Step title="Use local barrels for internal imports"> <Step title="Use local modules for internal imports">
Within your extension, create barrel files for internal code sharing instead Within your plugin, create local module files for internal code sharing
of importing through the plugin SDK: instead of re-importing through the plugin SDK:
```typescript ```typescript
// api.ts — public contract for this extension // api.ts — public exports for this plugin
export { MyChannelConfig } from "./src/config.js"; export { MyConfig } from "./src/config.js";
export { MyChannelRuntime } from "./src/runtime.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"; export { internalHelper } from "./src/helpers.js";
``` ```
<Warning> <Warning>
Never import your own extension back through its published SDK contract Never import your own plugin back through its published SDK path from
path from production files. Route internal imports through `./api.ts` or production files. Route internal imports through local files like `./api.ts`
`./runtime-api.ts` instead. The SDK contract is for external consumers only. or `./runtime-api.ts`. The SDK path is for external consumers only.
</Warning> </Warning>
</Step> </Step>
<Step title="Add a plugin manifest"> <Step title="Add a plugin manifest">
Create `openclaw.plugin.json` in your extension root: Create `openclaw.plugin.json` in your plugin root:
```json ```json
{ {
"id": "my-channel", "id": "my-plugin",
"kind": "channel", "kind": "provider",
"channels": ["my-channel"], "name": "My Plugin",
"name": "My Channel Plugin", "description": "Adds My Provider to OpenClaw"
"description": "Connects OpenClaw to My Channel"
} }
``` ```
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>
<Step title="Test with contract tests"> <Step title="Test your plugin">
OpenClaw runs contract tests against all registered plugins. After adding your **External plugins:** run your own test suite against the plugin SDK contracts.
extension, run:
**In-repo plugins:** OpenClaw runs contract tests against all registered plugins:
```bash ```bash
pnpm test:contracts:channels # channel plugins pnpm test:contracts:channels # channel plugins
pnpm test:contracts:plugins # provider plugins pnpm test:contracts:plugins # provider plugins
``` ```
Contract tests verify your plugin conforms to the expected interface (setup For unit tests, import test helpers from the testing surface:
wizard, session binding, message handling, group policy, etc.).
For unit tests, import test helpers from the public testing surface:
```typescript ```typescript
import { createTestRuntime } from "openclaw/plugin-sdk/testing"; import { createTestRuntime } from "openclaw/plugin-sdk/testing";
``` ```
</Step> </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> </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 1. **No monolithic root imports**`openclaw/plugin-sdk` root is rejected
2. **No direct src/ imports** — extensions cannot import `../../src/` directly 2. **No direct src/ imports**plugins cannot import `../../src/` directly
3. **No self-imports** — extensions cannot import their own `plugin-sdk/<name>` subpath 3. **No self-imports**plugins cannot import their own `plugin-sdk/\<name\>` subpath
Run `pnpm check` to verify all boundaries before committing. 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 ## Pre-submission checklist
<Check>**package.json** has correct `openclaw` metadata</Check> <Check>**package.json** has correct `openclaw` metadata</Check>
<Check>Entry point uses `defineChannelPluginEntry` or `definePluginEntry`</Check> <Check>Entry point uses `defineChannelPluginEntry` or `definePluginEntry`</Check>
<Check>All imports use focused `plugin-sdk/<subpath>` paths</Check> <Check>All imports use focused `plugin-sdk/\<subpath\>` paths</Check>
<Check>Internal imports use local barrels, not SDK self-imports</Check> <Check>Internal imports use local modules, not SDK self-imports</Check>
<Check>`openclaw.plugin.json` manifest is present and valid</Check> <Check>`openclaw.plugin.json` manifest is present and valid</Check>
<Check>Contract tests pass (`pnpm test:contracts`)</Check> <Check>Tests pass</Check>
<Check>Unit tests colocated as `*.test.ts`</Check> <Check>`pnpm check` passes (in-repo plugins)</Check>
<Check>`pnpm check` passes (lint + format)</Check>
<Check>Doc page created under `docs/channels/` or `docs/plugins/`</Check>
## Related ## 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 Architecture](/plugins/architecture) — internals and capability model
- [Plugin Manifest](/plugins/manifest) — full manifest schema - [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 ## 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: This caused:
- **Slow startup**: importing one helper pulled in dozens of unrelated modules. - **Slow startup**: importing one helper pulled in dozens of unrelated modules.
@ -28,14 +28,14 @@ with a clear purpose.
## What triggers the warning ## 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 [OPENCLAW_PLUGIN_SDK_COMPAT_DEPRECATED] Warning: openclaw/plugin-sdk/compat is
deprecated for new plugins. Migrate to focused openclaw/plugin-sdk/\<subpath\> imports. 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 error. But new plugins **must not** use it, and existing plugins should migrate
before compat is removed. 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: Each export from compat maps to a specific subpath. Replace the import source:
```typescript ```typescript
// Before (compat barrel) // Before (compat entry)
import { import {
createChannelReplyPipeline, createChannelReplyPipeline,
createPluginRuntimeStore, createPluginRuntimeStore,
@ -106,8 +106,8 @@ check the source at `src/plugin-sdk/` or ask in Discord.
## Compat barrel removal timeline ## Compat barrel removal timeline
- **Now**: compat barrel emits a deprecation warning at runtime. - **Now**: compat entry emits a deprecation warning at runtime.
- **Next major release**: compat barrel will be removed. Plugins still using it will - **Next major release**: compat entry will be removed. Plugins still using it will
fail to import. fail to import.
Bundled plugins (under `extensions/`) have already been migrated. External plugins Bundled plugins (under `extensions/`) have already been migrated. External plugins