diff --git a/docs/plugins/building-extensions.md b/docs/plugins/building-extensions.md
index bdbd384f192..7b4548194cd 100644
--- a/docs/plugins/building-extensions.md
+++ b/docs/plugins/building-extensions.md
@@ -9,8 +9,8 @@ read_when:
# Building Extensions
-This guide walks through creating an OpenClaw extension from scratch. Extensions
-can add channels, model providers, tools, or other capabilities.
+Extensions add channels, model providers, tools, or other capabilities to OpenClaw.
+This guide walks through creating one from scratch.
## Prerequisites
@@ -34,153 +34,165 @@ extensions/my-channel/
└── *.test.ts # Colocated tests
```
-## Step 1: Create the package
+## Create an extension
-Create `extensions/my-channel/package.json`:
+
+
+ Create `extensions/my-channel/package.json`:
-```json
-{
- "name": "@openclaw/my-channel",
- "version": "2026.1.1",
- "description": "OpenClaw My Channel plugin",
- "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"
+ ```json
+ {
+ "name": "@openclaw/my-channel",
+ "version": "2026.1.1",
+ "description": "OpenClaw My Channel plugin",
+ "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"
+ }
+ }
}
- }
-}
-```
+ ```
-The `openclaw` field tells the plugin system what your extension provides.
-For provider plugins, use `providers` instead of `channel`.
+ The `openclaw` field tells the plugin system what your extension provides.
+ For provider plugins, use `providers` instead of `channel`.
-## Step 2: Define the entry point
+
-Create `extensions/my-channel/index.ts`:
+
+ Create `extensions/my-channel/index.ts`:
-```typescript
-import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
+ ```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
- },
-});
-```
+ export default defineChannelPluginEntry({
+ id: "my-channel",
+ name: "My Channel",
+ description: "Connects OpenClaw to My Channel",
+ plugin: {
+ // Channel adapter implementation
+ },
+ });
+ ```
-For provider plugins, use `definePluginEntry` instead.
+ For provider plugins, use `definePluginEntry` instead.
-## Step 3: Import from focused subpaths
+
-The plugin SDK exposes many focused subpaths. Always import from specific
-subpaths rather than the monolithic root:
+
+ Always import from specific `openclaw/plugin-sdk/` paths rather than
+ the monolithic root. The old `openclaw/plugin-sdk/compat` barrel 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 { 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";
+ ```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 { 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)
-import { ... } from "openclaw/plugin-sdk";
-```
+ // Wrong: monolithic root (lint will reject this)
+ import { ... } from "openclaw/plugin-sdk";
+ ```
-Common subpaths:
+
+ | Subpath | Purpose |
+ | --- | --- |
+ | `plugin-sdk/core` | Plugin entry definitions, base types |
+ | `plugin-sdk/channel-setup` | Optional setup adapters/wizards |
+ | `plugin-sdk/channel-pairing` | DM pairing primitives |
+ | `plugin-sdk/channel-reply-pipeline` | Prefix + typing reply 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 |
+
-| Subpath | Purpose |
-| ----------------------------------- | ------------------------------------ |
-| `plugin-sdk/core` | Plugin entry definitions, base types |
-| `plugin-sdk/channel-setup` | Optional setup adapters/wizards |
-| `plugin-sdk/channel-pairing` | DM pairing primitives |
-| `plugin-sdk/channel-reply-pipeline` | Prefix + typing reply 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 |
+ 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 primitive that matches the job. Reach for `channel-runtime`
-or other larger helper barrels only when a dedicated subpath does not exist yet.
+
-## Step 4: Use local barrels for internal imports
+
+ Within your extension, create barrel files for internal code sharing instead
+ of importing through the plugin SDK:
-Within your extension, create barrel files for internal code sharing instead
-of 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";
-```typescript
-// api.ts — public contract for this extension
-export { MyChannelConfig } from "./src/config.js";
-export { MyChannelRuntime } from "./src/runtime.js";
+ // runtime-api.ts — internal-only exports (not for production consumers)
+ export { internalHelper } from "./src/helpers.js";
+ ```
-// runtime-api.ts — internal-only exports (not for production consumers)
-export { internalHelper } from "./src/helpers.js";
-```
+
+ 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.
+
-**Self-import guardrail**: 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.
+
-## Step 5: Add a plugin manifest
+
+ Create `openclaw.plugin.json` in your extension root:
-Create `openclaw.plugin.json` in your extension root:
+ ```json
+ {
+ "id": "my-channel",
+ "kind": "channel",
+ "channels": ["my-channel"],
+ "name": "My Channel Plugin",
+ "description": "Connects OpenClaw to My Channel"
+ }
+ ```
-```json
-{
- "id": "my-channel",
- "kind": "channel",
- "channels": ["my-channel"],
- "name": "My Channel Plugin",
- "description": "Connects OpenClaw to My Channel"
-}
-```
+ See [Plugin manifest](/plugins/manifest) for the full schema.
-See [Plugin manifest](/plugins/manifest) for the full schema.
+
-## Step 6: Test with contract tests
+
+ OpenClaw runs contract tests against all registered plugins. After adding your
+ extension, run:
-OpenClaw runs contract tests against all registered plugins. After adding your
-extension, run:
+ ```bash
+ pnpm test:contracts:channels # channel plugins
+ pnpm test:contracts:plugins # provider 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.).
-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 public testing surface:
+ ```typescript
+ import { createTestRuntime } from "openclaw/plugin-sdk/testing";
+ ```
-```typescript
-import { createTestRuntime } from "openclaw/plugin-sdk/testing";
-```
+
+
## Lint enforcement
@@ -192,16 +204,21 @@ Three scripts enforce SDK boundaries:
Run `pnpm check` to verify all boundaries before committing.
-## Checklist
+## Pre-submission checklist
-Before submitting your extension:
+**package.json** has correct `openclaw` metadata
+Entry point uses `defineChannelPluginEntry` or `definePluginEntry`
+All imports use focused `plugin-sdk/` paths
+Internal imports use local barrels, not SDK self-imports
+`openclaw.plugin.json` manifest is present and valid
+Contract tests pass (`pnpm test:contracts`)
+Unit tests colocated as `*.test.ts`
+`pnpm check` passes (lint + format)
+Doc page created under `docs/channels/` or `docs/plugins/`
-- [ ] `package.json` has correct `openclaw` metadata
-- [ ] Entry point uses `defineChannelPluginEntry` or `definePluginEntry`
-- [ ] All imports use focused `plugin-sdk/` paths
-- [ ] Internal imports use local barrels, not SDK self-imports
-- [ ] `openclaw.plugin.json` manifest is present and valid
-- [ ] Contract tests pass (`pnpm test:contracts`)
-- [ ] Unit tests colocated as `*.test.ts`
-- [ ] `pnpm check` passes (lint + format)
-- [ ] Doc page created under `docs/channels/` or `docs/plugins/`
+## Related
+
+- [Plugin SDK Migration](/plugins/sdk-migration) — migrating from compat to focused subpaths
+- [Plugin Architecture](/plugins/architecture) — internals and capability model
+- [Plugin Manifest](/plugins/manifest) — full manifest schema
+- [Community Plugins](/plugins/community) — existing community extensions