Files
openclaw/docs/plugins/building-plugins.md

10 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
You want to create a new OpenClaw plugin
You need to understand the plugin SDK import patterns
You are adding a new channel, provider, tool, or other capability to OpenClaw

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 install done

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)).
```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";
```

<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:

  1. No monolithic root importsopenclaw/plugin-sdk root is rejected
  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

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)