16 KiB
| title | sidebarTitle | summary | read_when | |||
|---|---|---|---|---|---|---|
| Plugin SDK Overview | SDK Overview | Import map, registration API reference, and SDK architecture |
|
Plugin SDK Overview
The plugin SDK is the typed contract between plugins and core. This page is the reference for what to import and what you can register.
**Looking for a how-to guide?** - First plugin? Start with [Getting Started](/plugins/building-plugins) - Channel plugin? See [Channel Plugins](/plugins/sdk-channel-plugins) - Provider plugin? See [Provider Plugins](/plugins/sdk-provider-plugins)Import convention
Always import from a specific subpath:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
Each subpath is a small, self-contained module. This keeps startup fast and
prevents circular dependency issues. For channel-specific entry/build helpers,
prefer openclaw/plugin-sdk/channel-core; keep openclaw/plugin-sdk/core for
the broader umbrella surface and shared helpers such as
buildChannelConfigSchema.
Do not add or depend on provider-named convenience seams such as
openclaw/plugin-sdk/slack, openclaw/plugin-sdk/discord,
openclaw/plugin-sdk/signal, or openclaw/plugin-sdk/whatsapp. Bundled plugins should compose generic SDK
subpaths inside their own api.ts or runtime-api.ts barrels, and core should
either use those plugin-local barrels or add a narrow generic SDK contract when
the need is truly cross-channel.
Subpath reference
The most commonly used subpaths, grouped by purpose. The full list of 100+
subpaths is in scripts/lib/plugin-sdk-entrypoints.json.
Plugin entry
| Subpath | Key exports |
|---|---|
plugin-sdk/plugin-entry |
definePluginEntry |
plugin-sdk/core |
defineChannelPluginEntry, createChatChannelPlugin, createChannelPluginBase, defineSetupPluginEntry, buildChannelConfigSchema |
Registration API
The register(api) callback receives an OpenClawPluginApi object with these
methods:
Capability registration
| Method | What it registers |
|---|---|
api.registerProvider(...) |
Text inference (LLM) |
api.registerCliBackend(...) |
Local CLI inference backend |
api.registerChannel(...) |
Messaging channel |
api.registerSpeechProvider(...) |
Text-to-speech / STT synthesis |
api.registerRealtimeTranscriptionProvider(...) |
Streaming realtime transcription |
api.registerRealtimeVoiceProvider(...) |
Duplex realtime voice sessions |
api.registerMediaUnderstandingProvider(...) |
Image/audio/video analysis |
api.registerImageGenerationProvider(...) |
Image generation |
api.registerWebSearchProvider(...) |
Web search |
Tools and commands
| Method | What it registers |
|---|---|
api.registerTool(tool, opts?) |
Agent tool (required or { optional: true }) |
api.registerCommand(def) |
Custom command (bypasses the LLM) |
Infrastructure
| Method | What it registers |
|---|---|
api.registerHook(events, handler, opts?) |
Event hook |
api.registerHttpRoute(params) |
Gateway HTTP endpoint |
api.registerGatewayMethod(name, handler) |
Gateway RPC method |
api.registerCli(registrar, opts?) |
CLI subcommand |
api.registerService(service) |
Background service |
api.registerInteractiveHandler(registration) |
Interactive handler |
CLI registration metadata
api.registerCli(registrar, opts?) accepts two kinds of top-level metadata:
commands: explicit command roots owned by the registrardescriptors: parse-time command descriptors used for root CLI help, routing, and lazy plugin CLI registration
If you want a plugin command to stay lazy-loaded in the normal root CLI path,
provide descriptors that cover every top-level command root exposed by that
registrar.
api.registerCli(
async ({ program }) => {
const { registerMatrixCli } = await import("./src/cli.js");
registerMatrixCli({ program });
},
{
descriptors: [
{
name: "matrix",
description: "Manage Matrix accounts, verification, devices, and profile state",
hasSubcommands: true,
},
],
},
);
Use commands by itself only when you do not need lazy root CLI registration.
That eager compatibility path remains supported, but it does not install
descriptor-backed placeholders for parse-time lazy loading.
CLI backend registration
api.registerCliBackend(...) lets a plugin own the default config for a local
AI CLI backend such as claude-cli or codex-cli.
- The backend
idbecomes the provider prefix in model refs likeclaude-cli/opus. - The backend
configuses the same shape asagents.defaults.cliBackends.<id>. - User config still wins. OpenClaw merges
agents.defaults.cliBackends.<id>over the plugin default before running the CLI. - Use
normalizeConfigwhen a backend needs compatibility rewrites after merge (for example normalizing old flag shapes).
Exclusive slots
| Method | What it registers |
|---|---|
api.registerContextEngine(id, factory) |
Context engine (one active at a time) |
api.registerMemoryPromptSection(builder) |
Memory prompt section builder |
api.registerMemoryFlushPlan(resolver) |
Memory flush plan resolver |
api.registerMemoryRuntime(runtime) |
Memory runtime adapter |
Memory embedding adapters
| Method | What it registers |
|---|---|
api.registerMemoryEmbeddingProvider(adapter) |
Memory embedding adapter for the active plugin |
registerMemoryPromptSection,registerMemoryFlushPlan, andregisterMemoryRuntimeare exclusive to memory plugins.registerMemoryEmbeddingProviderlets the active memory plugin register one or more embedding adapter ids (for exampleopenai,gemini, or a custom plugin-defined id).- User config such as
agents.defaults.memorySearch.providerandagents.defaults.memorySearch.fallbackresolves against those registered adapter ids.
Events and lifecycle
| Method | What it does |
|---|---|
api.on(hookName, handler, opts?) |
Typed lifecycle hook |
api.onConversationBindingResolved(handler) |
Conversation binding callback |
Hook decision semantics
before_tool_call: returning{ block: true }is terminal. Once any handler sets it, lower-priority handlers are skipped.before_tool_call: returning{ block: false }is treated as no decision (same as omittingblock), not as an override.before_install: returning{ block: true }is terminal. Once any handler sets it, lower-priority handlers are skipped.before_install: returning{ block: false }is treated as no decision (same as omittingblock), not as an override.message_sending: returning{ cancel: true }is terminal. Once any handler sets it, lower-priority handlers are skipped.message_sending: returning{ cancel: false }is treated as no decision (same as omittingcancel), not as an override.
API object fields
| Field | Type | Description |
|---|---|---|
api.id |
string |
Plugin id |
api.name |
string |
Display name |
api.version |
string? |
Plugin version (optional) |
api.description |
string? |
Plugin description (optional) |
api.source |
string |
Plugin source path |
api.rootDir |
string? |
Plugin root directory (optional) |
api.config |
OpenClawConfig |
Current config snapshot (active in-memory runtime snapshot when available) |
api.pluginConfig |
Record<string, unknown> |
Plugin-specific config from plugins.entries.<id>.config |
api.runtime |
PluginRuntime |
Runtime helpers |
api.logger |
PluginLogger |
Scoped logger (debug, info, warn, error) |
api.registrationMode |
PluginRegistrationMode |
"full", "setup-only", "setup-runtime", or "cli-metadata" |
api.resolvePath(input) |
(string) => string |
Resolve path relative to plugin root |
Internal module convention
Within your plugin, use local barrel files for internal imports:
my-plugin/
api.ts # Public exports for external consumers
runtime-api.ts # Internal-only runtime exports
index.ts # Plugin entry point
setup-entry.ts # Lightweight setup-only entry (optional)
Never import your own plugin through `openclaw/plugin-sdk/`
from production code. Route internal imports through `./api.ts` or
`./runtime-api.ts`. The SDK path is the external contract only.
Facade-loaded bundled plugin public surfaces (api.ts, runtime-api.ts,
index.ts, setup-entry.ts, and similar public entry files) now prefer the
active runtime config snapshot when OpenClaw is already running. If no runtime
snapshot exists yet, they fall back to the resolved config file on disk.
Related
- Entry Points —
definePluginEntryanddefineChannelPluginEntryoptions - Runtime Helpers — full
api.runtimenamespace reference - Setup and Config — packaging, manifests, config schemas
- Testing — test utilities and lint rules
- SDK Migration — migrating from deprecated surfaces
- Plugin Internals — deep architecture and capability model