Skip to main content

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:
CapabilityRegistration methodWhat it adds
Text inferenceapi.registerProvider(...)Model provider (LLM)
Channel / messagingapi.registerChannel(...)Chat channel (e.g. Slack, IRC)
Speechapi.registerSpeechProvider(...)Text-to-speech / STT
Media understandingapi.registerMediaUnderstandingProvider(...)Image/audio/video analysis
Image generationapi.registerImageGenerationProvider(...)Image generation
Web searchapi.registerWebSearchProvider(...)Web search provider
Agent toolsapi.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

1

Create the package

Create package.json with the openclaw metadata block. The structure depends on what capabilities your plugin provides.Channel plugin example:
{
  "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:
{
  "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.
2

Define the entry point

The entry point registers your capabilities with the plugin API.Channel plugin:
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:
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):
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.
3

Import from focused SDK subpaths

Always import from specific openclaw/plugin-sdk/\<subpath\> paths. The old monolithic import is deprecated (see 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.
// 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";
SubpathPurpose
plugin-sdk/corePlugin entry definitions and base types
plugin-sdk/channel-setupSetup wizard adapters
plugin-sdk/channel-pairingDM pairing primitives
plugin-sdk/channel-reply-pipelineReply prefix + typing wiring
plugin-sdk/channel-config-schemaConfig schema builders
plugin-sdk/channel-policyGroup/DM policy helpers
plugin-sdk/secret-inputSecret input parsing/helpers
plugin-sdk/webhook-ingressWebhook request/target helpers
plugin-sdk/runtime-storePersistent plugin storage
plugin-sdk/allow-fromAllowlist resolution
plugin-sdk/reply-payloadMessage reply types
plugin-sdk/provider-oauthOAuth login + PKCE helpers
plugin-sdk/provider-onboardProvider onboarding config patches
plugin-sdk/testingTest utilities
Use the narrowest subpath that matches the job.
4

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:
// 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";
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.
5

Add a plugin manifest

Create openclaw.plugin.json in your plugin root:
{
  "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 for the full schema.
6

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:
pnpm test:contracts:channels   # channel plugins
pnpm test:contracts:plugins    # provider plugins
For unit tests, import test helpers from the testing surface:
import { createTestRuntime } from "openclaw/plugin-sdk/testing";
7

Publish and install

External plugins: publish to npm, then install:
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:
openclaw plugins search <query>
openclaw plugins install <npm-spec>

Registering agent tools

Plugins can register agent tools — typed functions the LLM can call. Tools can be required (always available) or optional (users opt in via allowlists).
import { Type } from "@sinclair/typebox";

export default definePluginEntry({
  id: "my-plugin",
  name: "My Plugin",
  register(api) {
    // Required tool (always available)
    api.registerTool({
      name: "my_tool",
      description: "Do a thing",
      parameters: Type.Object({ input: Type.String() }),
      async execute(_id, params) {
        return { content: [{ type: "text", text: params.input }] };
      },
    });

    // Optional tool (user must add to allowlist)
    api.registerTool(
      {
        name: "workflow_tool",
        description: "Run a workflow",
        parameters: Type.Object({ pipeline: Type.String() }),
        async execute(_id, params) {
          return { content: [{ type: "text", text: params.pipeline }] };
        },
      },
      { optional: true },
    );
  },
});
Enable optional tools in config:
{
  tools: { allow: ["workflow_tool"] },
}
Tips:
  • Tool names must not clash with core tool names (conflicts are skipped)
  • Use optional: true for tools that trigger side effects or require extra binaries
  • Users can enable all tools from a plugin by adding the plugin id to tools.allow

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)