Plugin SDK Overview: Import Map, Registration API, and Architecture
This page is a reference for plugin developers on which imports to use and what registrations are available in the Plugin SDK. It covers the typed interface between the core system and plugins.
Read this when
- You need to know which SDK subpath to import from
- You want a reference for all registration methods on OpenClawPluginApi
- You are looking up a specific SDK export
The Plugin SDK defines a typed interface between the core system and its plugins. This page serves as a reference for which imports to use and what registrations are available.
Note
This documentation targets plugin developers working with
openclaw/plugin-sdk/*inside OpenClaw. For external applications, scripts, dashboards, CI pipelines, and IDE extensions that need to interact with agents via the Gateway, refer to Gateway integrations for external apps instead.
Tip
Prefer a practical tutorial? Begin with Building plugins. Use Channel plugins for messaging channels, Provider plugins for model providers, CLI backend plugins for local AI CLI backends, Agent harness plugins for native agent executors, and Plugin hooks for tool or lifecycle hooks.
Import convention
Always import from a dedicated subpath:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
Each subpath is a compact, self-contained module. This approach ensures fast startup times and avoids circular dependency problems. For channel-specific entry and build utilities, prefer openclaw/plugin-sdk/channel-core; reserve openclaw/plugin-sdk/core for the general umbrella surface and shared utilities like buildChannelConfigSchema.
When defining channel configuration, expose the channel-owned JSON Schema via openclaw.plugin.json#channelConfigs. The plugin-sdk/channel-config-schema subpath provides shared schema primitives and the generic builder. OpenClaw's bundled plugins rely on plugin-sdk/bundled-channel-config-schema for their retained bundled-channel schemas. That bundled schema subpath is not intended as a pattern for new plugins.
Warning
Do not import provider- or channel-specific convenience seams (for example
openclaw/plugin-sdk/slack,.../discord,.../signal,api.ts/runtime-api.tsbarrels; core consumers should either use those plugin-local barrels or add a narrow generic SDK contract when a need is genuinely cross-channel.A limited set of bundled-plugin helper seams still appear in the generated export map when they have tracked owner usage. These exist solely for bundled-plugin maintenance and are not recommended import paths for new third-party plugins.
openclaw/plugin-sdk/discordandopenclaw/plugin-sdk/telegram-accountare also retained as deprecated compatibility facades for tracked owner usage. Do not copy those import paths into new plugins; use injected runtime helpers and generic channel SDK subpaths instead.
Subpath reference
The plugin SDK is published as a collection of narrow subpaths organized by area (plugin entry, channel, provider, auth, runtime, capability, memory, and reserved bundled-plugin helpers). For the complete catalog, organized and linked, see Plugin SDK subpaths.
The compiler entrypoint inventory is located in scripts/lib/plugin-sdk-entrypoints.json; typed public exports exclude the internal subpaths listed in scripts/lib/plugin-sdk-private-local-only-subpaths.json. Production entries on that list retain JavaScript-only host runtime exports for separately published official plugins, while test-only entries remain unexported. Run pnpm plugin-sdk:surface to audit the public export count. Deprecated public subpaths that are sufficiently old and unused by bundled extension production code are tracked in scripts/lib/plugin-sdk-deprecated-public-subpaths.json; broad deprecated re-export barrels are tracked in scripts/lib/plugin-sdk-deprecated-barrel-subpaths.json.
Registration API
The register(api) callback receives an OpenClawPluginApi object with these methods:
Plugins offering an external team-chat surface for a session can register the single process-wide provider exported by openclaw/plugin-sdk/session-discussion. Its info({ sessionKey }) method indicates whether a discussion is unavailable, ready to open, or already open; open({ sessionKey }) creates or resolves the discussion and returns its embed and external URLs. Registering a new provider replaces the current one.
Capability registration
| Method | What it registers |
|---|---|
api.registerProvider(...) | Text inference (LLM) |
api.registerWorkerProvider(...) | Cloud-worker lifecycle leases |
api.registerModelCatalogProvider(...) | Model catalog rows for text and media generation |
api.registerAgentHarness(...) | Experimental native agent executor (Codex, Copilot) |
api.registerCliBackend(...) | Local CLI inference backend |
api.registerChannel(...) | Messaging channel |
api.registerEmbeddingProvider(...) | Reusable vector embedding provider |
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.registerTranscriptSourceProvider(...) | Live or imported meeting transcript source; meeting plugins can use createMeetingTranscriptSourceProvider from plugin-sdk/transcripts |
api.registerImageGenerationProvider(...) | Image generation |
api.registerMusicGenerationProvider(...) | Music generation |
api.registerVideoGenerationProvider(...) | Video generation |
api.registerWebFetchProvider(...) | Web fetch / scrape provider |
api.registerWebSearchProvider(...) | Web search |
api.registerCompactionProvider(...) | Pluggable transcript-compaction backend |
Worker providers are required to declare their id in contracts.workerProviders.
Before provision(profile, operationId), Core persists durable intent. Providers validate settings before external allocation and throw WorkerProviderError when a profile is permanently rejected. provision must adopt the same lease if the operation id repeats.
Core persists the validated profile settings together with the lease and provides that snapshot to destroy({ leaseId, profile }), which must be idempotent, and inspect({ leaseId, profile }), which returns active, destroyed, or unknown. This allows providers to route lifecycle calls after a gateway restart or named-profile removal. SSH endpoints use a SecretRef for keyRef, never embed key material inline, and include a hostKey from trusted provisioning output exactly as algorithm base64, without a hostname or comment. Core pins hostKey and never trusts a key from the first connection. A provider that creates a dynamic keyRef can implement resolveSshIdentity({ leaseId, profile, keyRef }); when present, that resolver is authoritative, while providers without it use the configured generic secret resolver.
Providers with renewable leases can also implement renew(leaseId).
inspect must throw on transient or indeterminate failures; return unknown only for authoritative absence. Core marks an active local record as orphaned, or treats the absence as teardown completion after a persisted destroy request.
Embedding providers registered with api.registerEmbeddingProvider(...) must also be listed in contracts.embeddingProviders in the plugin manifest. This is the generic embedding surface for reusable vector generation. Memory search can consume this generic provider surface. The older api.registerMemoryEmbeddingProvider(...) and contracts.memoryEmbeddingProviders seam is deprecated compatibility while existing memory-specific providers migrate.
Memory-specific providers that still expose a runtime batchEmbed(...) remain on the existing per-file batching contract unless their runtime explicitly sets sourceWideBatchEmbed: true. That opt-in allows the memory host to submit chunks from multiple dirty memory files and enabled sources in one batchEmbed(...) call up to the host batch limits. Batch adapters that upload JSONL request files must split provider jobs before their upload-size cap as well as their request-count cap. The provider must return one embedding per input chunk in the same order as batch.chunks; omit the flag when the provider expects file-local batches or cannot preserve input ordering across a larger source-wide job.
Tools and commands
Use defineToolPlugin for simple tool-only plugins with fixed tool names. Use api.registerTool(...) directly for mixed plugins or fully dynamic tool registration.
| Method | What it registers |
|---|---|
api.registerTool(tool, opts?) | Agent tool (required or { optional: true }) |
api.registerCommand(def) | Custom command (bypasses the LLM) |
api.registerNodeHostCommand(command) | Command handled by openclaw node run; optional agentTool metadata can expose it as an agent-visible tool while the node is connected |
Plugin commands can set agentPromptGuidance when the agent needs a short, command-owned routing hint. Keep that text about the command itself; do not add provider- or plugin-specific policy to core prompt builders.
Guidance entries may be legacy strings, which apply to every prompt surface, or structured entries:
agentPromptGuidance: [
"Global command hint.",
{ text: "Only show this in the main OpenClaw prompt.", surfaces: ["openclaw_main"] },
];
Structured surfaces may include openclaw_main, codex_app_server, cli_backend, acp_backend, or subagent. pi_main remains a deprecated alias for openclaw_main. Omit surfaces for intentional all-surface guidance. Do not pass an empty surfaces array; it is rejected so accidental scope loss does not become global prompt text.
Native Codex app-server developer instructions are stricter than other prompt surfaces: only guidance explicitly scoped to codex_app_server is promoted into that higher-priority lane. Legacy string guidance and unscoped structured guidance remain available to non-Codex prompt surfaces for compatibility.
Node-host commands run on the connected node host, not inside the Gateway process. If agentTool is present, the node publishes a descriptor after a successful Gateway connect; the Gateway exposes it to agent runs only while that node is connected and only if the descriptor's command is in the node's approved command surface. Set agentTool.defaultPlatforms to opt a non-dangerous command into the default node command allowlist; otherwise require explicit gateway.nodes.commands.allow or a node-invoke policy. agentTool.name must be provider-safe: start with a letter, use only letters, digits, underscores, or hyphens, and stay within 64 characters. MCP-backed node tools can set agentTool.mcp metadata so catalog and tool-search surfaces can show the remote MCP server/tool identity, but execution still goes through the advertised node command.
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.registerGatewayDiscoveryService(service) | Local Gateway discovery advertiser |
api.registerCli(registrar, opts?) | CLI subcommand |
api.registerNodeCliFeature(registrar, opts?) | Node feature CLI under openclaw nodes |
api.registerService(service) | Background service |
api.registerInteractiveHandler(registration) | Interactive handler |
api.registerAgentToolResultMiddleware(...) | Runtime tool-result middleware |
api.registerMemoryPromptSupplement(builder) | Additive memory-adjacent prompt section |
api.registerMemoryPromptPreparation(prepare) | Async preparation for a memory-adjacent prompt section |
api.registerMemoryCorpusSupplement(adapter) | Additive memory search/read corpus |
api.registerHostedMediaResolver(resolver) | Resolver for browser-style hosted media URLs |
api.registerMcpServerConnectionResolver(...) | Per-requester MCP transport (url/headers) for a static server name |
api.registerTextTransforms(transforms) | Plugin-owned prompt/message compatibility text rewrites |
api.registerConfigMigration(migrate) | Lightweight config migration run before plugin runtime loads |
api.registerMigrationProvider(provider) | Importer for openclaw migrate |
api.registerAutoEnableProbe(probe) | Config probe that can auto-enable this plugin |
api.registerReload(registration) | Restart/hot/noop config-prefix policy for reload handling |
api.registerNodeHostCommand(command) | Command handler exposed to paired nodes |
api.registerNodeInvokePolicy(policy) | Allowlist/approval policy for node-invoked commands |
api.registerSecurityAuditCollector(collector) | Findings collector for openclaw security audit |
Post-ack webhook work
For webhook routes that acknowledge a request before processing finishes, the detached work must be placed on its own tracked admission root:
import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
void runDetachedWebhookWork(() => processWebhookEvent(event)).catch((error) => {
runtime.error?.(`webhook dispatch failed: ${String(error)}`);
});
Invoke runDetachedWebhookWork(...) synchronously while the HTTP request is still admitted. The helper immediately reserves an independent root, then starts the callback in the next microtask so the request handler can write its acknowledgement first. The returned promise adopts the callback result; callers still own rejection handling. This keeps post-ack queue work accepted and makes restart or suspension drains wait for it. Handlers that await all processing before returning do not need this helper.
Requester-scoped MCP connections
Keep the MCP server identity static (name, tool filter) in mcp.servers, a native plugin's mcpServers manifest field, or a bundle manifest. Optionally register a connection resolver so each trusted message requester gets their own transport:
api.registerMcpServerConnectionResolver({
serverName: "user-email",
resolve: async (ctx) => {
// ctx.requesterSenderId is host-trusted; never invent sender identity here.
const token = await lookupUserToken(ctx.requesterSenderId);
if (!token) {
return null; // omit this server for the current run
}
return {
url: "https://mcp.example.com/email",
headers: { Authorization: `Bearer ${token}` },
};
},
});
Contract notes:
- Resolver context carries trusted host identity only (
requesterSenderId, optionalagentAccountId/messageChannel). Future trusted fields (for example cron/subagent user context) can be added additively. - One plugin owns one server name: a duplicate
registerMcpServerConnectionResolverfor the sameserverNamefrom another plugin is rejected with an error diagnostic (first registration wins), so connection ownership never depends on plugin load order. - Tool names are derived from the full declared server set so partial resolution never changes safe server names between requesters or turns. Core does not verify that different requester endpoints serve identical tool schemas; a resolver must point every requester at the same logical service, or tool schemas (and prompt-cache stability) diverge per requester.
- Runs without a trusted
requesterSenderId(cron, subagent, heartbeat, public gateway) never materialize requester-scoped servers. There is no shared fallback connection. resolveis bounded at 10 seconds per server; a timeout or throw omits that server for the run without failing static MCP.- Resolved connections are revalidated at most every 5 minutes per requester: rotation rebuilds the transport with fresh credentials, and a
nullresult revokes it (the cached runtime is disposed even mid-session). A revoked or rotated credential can therefore stay in use for up to 5 minutes. - Resolved
headersare never logged or persisted; core keeps only an ephemeral in-memory keyed digest (process-local HMAC) to detect credential rotation, and registers resolved header/URL credential values with the log/debug-capture redaction registry. - Requester-scoped servers do not mint MCP App views: a view outlives the requester-authenticated run and the gateway view boundary has no requester identity, so app previews stay fail-closed for these servers. Tool results are unaffected.
- Static servers without a resolver keep the existing session-scoped lifecycle.
- Harness delivery rule: requester-scoped servers never enter harness-native MCP client config (Codex thread
mcp_servers, CLI-c mcp_servers=…, or any other session-shared MCP projection). Harnesses deliver them as run-scoped tools instead:- Embedded runner: session MCP runtime + bundle tools (static + scoped).
- Codex app-server: dynamic tools via
materializeRequesterScopedMcpToolsForHarnessRun(scoped-only; static servers stay on Codex's native MCP client).
- Scoped tool specs are session-stable after the first successful resolve in that session, so shared-thread harnesses (Codex) do not rotate threads when senders change. Before any requester resolves, no scoped specs are advertised.
- Unauthenticated requesters on a shared-thread harness still see the advertised scoped tools; calling one returns a clean not-connected tool error for that requester. OpenClaw never falls back to another requester's credentials.
Memory prompt supplement builders receive optional agentId, agentSessionKey, and sandboxed context. Memory corpus supplement search and get calls receive optional agentId and sandboxed context. Plugins with agent-owned storage should resolve that storage for each call instead of capturing one global path during registration. If an agent id is required but missing in a multi-agent operation, fail closed rather than choosing an arbitrary agent.
Use registerMemoryPromptPreparation(...) when prompt text depends on async plugin state. The callback runs once before each full agent prompt and receives the same tool, agent, session, and sandbox context as synchronous memory prompt builders. Validate the current storage-owner instance before loading persisted state, then return only lines for that run. OpenClaw freezes those lines and hands the immutable result to synchronous prompt assembly. Keep persistence, atomic replacement, and owner-removal deletion inside the owning plugin; do not poll or read files from a prompt builder.
Telegram interactive handlers can return { submitText } to route text through Telegram's normal inbound agent path after the handler succeeds. OpenClaw keeps the callback button when inbound policy skips the text or processing fails, so the user can retry after the blocking condition changes. This result field is Telegram-specific; other channels keep their own interactive result contracts.
Host hooks for workflow plugins
Host hooks serve as the SDK integration points for plugins that need to engage with the host lifecycle rather than only adding a provider, channel, or tool. These are generic contracts; Plan Mode can use them, but so can approval workflows, workspace policy gates, background monitors, setup wizards, and UI companion plugins.
| Method | Contract it owns |
|---|---|
api.session.state.registerSessionExtension(...) | Session state owned by the plugin, JSON-compatible, projected through Gateway sessions |
api.session.workflow.enqueueNextTurnInjection(...) | Exactly-once durable context injected into the next agent turn for a single session |
api.registerTrustedToolPolicy(...) | Manifest-gated trusted pre-plugin tool policy that can block or rewrite tool parameters |
api.registerToolMetadata(...) | Tool catalog display metadata without altering the tool implementation |
api.registerCommand(...) | Scoped plugin commands; command results can set continueAgent: true or suppressReply: true; Discord native commands support descriptionLocalizations |
api.session.controls.registerControlUiDescriptor(...) | Control UI contribution descriptors for session, tool, run, settings, or tab surfaces |
api.lifecycle.registerRuntimeLifecycle(...) | Cleanup callbacks for plugin-owned runtime resources on reset, delete, or reload paths |
api.agent.events.registerAgentEventSubscription(...) | Sanitized event subscriptions for workflow state and monitors |
api.runContext.setRunContext(...) / getRunContext(...) / clearRunContext(...) | Per-run plugin scratch state cleared when the run lifecycle reaches a terminal state |
api.session.workflow.registerSessionSchedulerJob(...) | Cleanup metadata for plugin-owned scheduler jobs; does not schedule work or create task records |
api.session.workflow.sendSessionAttachment(...) | Bundled-only host-mediated file attachment delivery to the active direct-outbound session route |
api.session.workflow.scheduleSessionTurn(...) / unscheduleSessionTurnsByTag(...) | Bundled-only Cron-backed scheduled session turns plus tag-based cleanup |
api.session.controls.registerSessionAction(...) | Typed session actions clients can dispatch through the Gateway |
A surface: "tab" descriptor adds a sidebar tab to the Control UI. Active
plugins advertise their tab descriptors to dashboard clients in the gateway
hello (controlUiTabs), so the tab appears only while the plugin is enabled.
Bundled plugins may ship a first-class dashboard view for their tab; other
plugins can set path to a plugin HTTP route (see
api.registerHttpRoute(...)) that the dashboard renders in a sandboxed frame.
icon is a dashboard icon name hint, group picks the sidebar section
(control or agent), order sorts among plugin tabs, and requiredScopes
hides the tab from connections lacking those operator scopes:
For a gateway-protected external tab, register the descriptor path under a
same-plugin auth: "gateway" HTTP route. After authenticated bootstrap, the browser gets a
short-lived, HttpOnly grant scoped to that plugin and route root so the
sandboxed frame can load without copying the Gateway bearer token into its URL
or JavaScript. The authenticated parent renews the grant while the external tab
is active and before mounting it after navigation or browser resume. It also
probes the grant from the same opaque sandbox before mounting, so browser
privacy modes that block the cookie fail closed with an unavailable panel.
The frame grant accepts only GET and HEAD and always carries
operator.read; requiredScopes controls tab visibility but never widens the
cookie grant. Mutations remain on explicit Gateway-authenticated parent or
bearer surfaces. External tabs require HTTPS/Tailscale Serve or a
browser-trusted loopback origin; plain HTTP on a LAN host shows the
secure-context error instead of mounting a panel that cannot authenticate.
Full third-party-cookie blocking also makes gateway-protected tabs unavailable.
As with all native plugin surfaces, the frame remains inside the installed
plugin trust boundary; OpenClaw does not treat installed plugins as mutually
isolated browser security principals.
Cookie grants use the browser's hostname boundary, not its port boundary. Do
not cohost mutually untrusted services on the Gateway hostname, even on other
ports.
Tabs backed by plugin-managed auth keep their direct iframe behavior and do not
request or require this Gateway grant.
api.session.controls.registerControlUiDescriptor({
surface: "tab",
id: "logbook",
label: "Logbook",
description: "Your day as a timeline, built from screen snapshots.",
icon: "sun",
group: "control",
requiredScopes: ["operator.write"],
});
Use the grouped namespaces for new plugin code:
api.session.state.registerSessionExtension(...)api.session.workflow.enqueueNextTurnInjection(...)api.session.workflow.registerSessionSchedulerJob(...)api.session.workflow.sendSessionAttachment(...)api.session.workflow.scheduleSessionTurn(...)api.session.workflow.unscheduleSessionTurnsByTag(...)api.session.controls.registerSessionAction(...)api.session.controls.registerControlUiDescriptor(...)api.agent.events.registerAgentEventSubscription(...)api.agent.events.emitAgentEvent(...)api.runContext.setRunContext(...)/getRunContext(...)/clearRunContext(...)api.lifecycle.registerRuntimeLifecycle(...)
The deprecated flat method equivalents are still accessible as compatibility aliases for legacy plugins. Refrain from introducing new plugin code that directly invokes api.registerSessionExtension, api.enqueueNextTurnInjection, api.registerControlUiDescriptor, api.registerRuntimeLifecycle, api.registerAgentEventSubscription, api.emitAgentEvent, api.setRunContext, api.getRunContext, api.clearRunContext, api.registerSessionSchedulerJob, api.registerSessionAction, api.sendSessionAttachment, api.scheduleSessionTurn, or api.unscheduleSessionTurnsByTag.
scheduleSessionTurn(...) provides a session-scoped convenience layer over the Gateway Cron scheduler. Cron manages the timing and generates the background task record when the turn executes; the Plugin SDK only restricts the target session, plugin-owned naming, and cleanup. Use api.runtime.tasks.managedFlows inside the scheduled turn when the work requires durable multi-step Task Flow state.
The contracts deliberately separate authority:
- External plugins may own session extensions, UI descriptors, commands, tool metadata, next-turn injections, and standard hooks.
- Trusted tool policies execute before regular
before_tool_callhooks and are host-trusted. Bundled policies run first; installed-plugin policies need explicit enablement plus their local identifiers incontracts.trustedToolPolicies, and execute next in plugin-load order. Policy ids are confined to the registering plugin. - Reserved command ownership is limited to bundled plugins. External plugins should use their own command names or aliases.
allowPromptInjection=falsedisables prompt-mutating hooks, which includeagent_turn_prepare,before_prompt_build,heartbeat_prompt_contribution, andenqueueNextTurnInjection.
Examples of non-Plan consumers:
| Plugin archetype | Hooks used |
|---|---|
| Approval workflow | Session extension, command continuation, next-turn injection, UI descriptor |
| Budget/workspace policy gate | Trusted tool policy, tool metadata, session projection |
| Background lifecycle monitor | Runtime lifecycle cleanup, agent event subscription, session scheduler ownership/cleanup, heartbeat prompt contribution, UI descriptor |
| Setup or onboarding wizard | Session extension, scoped commands, Control UI descriptor |
Note
Reserved core admin namespaces (
config.*,exec.approvals.*,wizard.*,update.*) always remainoperator.admin, even if a plugin attempts to assign a narrower gateway method scope. Prefer plugin-specific prefixes for plugin-owned methods.
When to use tool-result middleware
Bundled plugins and explicitly enabled installed plugins with matching manifest contracts can utilize api.registerAgentToolResultMiddleware(...) when they need to modify a tool result after execution and before the runtime feeds that result back into the model. This provides the trusted runtime-neutral seam for async output reducers such as tokenjuice.
Plugins must declare contracts.agentToolResultMiddleware for each targeted runtime, for instance ["openclaw", "codex"]. Installed plugins lacking that contract, or without explicit enablement, cannot register this middleware; use normal OpenClaw plugin hooks for work that does not require pre-model tool-result timing. The old embedded-runner-only extension factory registration path has been removed.
Gateway discovery registration
api.registerGatewayDiscoveryService(...) enables a plugin to advertise the active Gateway on a local discovery transport such as mDNS/Bonjour. OpenClaw calls the service during Gateway startup when local discovery is enabled, passes the current Gateway ports and non-secret TXT hint data, and invokes the returned stop handler during Gateway shutdown.
api.registerGatewayDiscoveryService({
id: "my-discovery",
async advertise(ctx) {
const handle = await startMyAdvertiser({
gatewayPort: ctx.gatewayPort,
tls: ctx.gatewayTlsEnabled,
displayName: ctx.machineDisplayName,
});
return { stop: () => handle.stop() };
},
});
Gateway discovery plugins must not treat advertised TXT values as secrets or authentication. Discovery serves as a routing hint; Gateway auth and TLS pinning still control trust.
CLI registration metadata
api.registerCli(registrar, opts?) accepts two kinds of command metadata:
commands: explicit command names owned by the registrardescriptors: parse-time command descriptors used for CLI help, routing, and lazy plugin CLI registrationparentPath: optional parent command path for nested command groups, such as["nodes"]
For paired-node features, prefer api.registerNodeCliFeature(registrar, opts?). It acts as a small wrapper around api.registerCli(..., { parentPath: ["nodes"] }) and makes commands such as openclaw nodes canvas explicit plugin-owned node features.
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,
},
],
},
);
Nested commands receive the resolved parent command as program:
api.registerCli(
async ({ program }) => {
const { registerNodesCanvasCommands } = await import("./src/cli.js");
registerNodesCanvasCommands(program);
},
{
parentPath: ["nodes"],
descriptors: [
{
name: "canvas",
description: "Capture or render canvas content from a paired node",
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 my-cli.
- The backend
idbecomes the provider prefix inside model references such asmy-cli/gpt-5. - As the authoritative command adapter, the backend
confighouses argv, environment, parser, session, image, and reliability logic within plugin code. - Model refs or model-scoped
agentRuntime.idlet users choose the backend;openclaw.jsondoes not modify the adapter. - When registered static fields need a runtime-aware normalization pass, use
normalizeConfig. - For request-scoped argv rewrites tied to the CLI dialect (for instance, translating OpenClaw thinking levels into a native effort flag), use
resolveExecutionArgs. The hook is givenctx.executionMode; apply"side-question"to add backend-native isolation flags for ephemeral/btwcalls. If those flags reliably turn off native tools for an always-on CLI, also declaresideQuestionToolMode: "disabled". - Use
prepareExecutionfor backend-owned launch environment or temporary auth/config bridges. Itsctx.contextTokenBudgetholds the effective token limit chosen for the run, letting native-compaction backends align their own threshold without provider-specific core branches. It also receives the core-preparedctx.envwhen backend staging needs to extend bundled MCP settings. - Backends capable of disabling all native tools for a specific run can declare
nativeToolMode: "selectable". Restricted calls pass an exactctx.toolAvailability.nativelist plus canonicalctx.toolAvailability.openClawnames. DeclaretoolAvailabilityEnforcement: "execution-args"and enforce the contract in final fresh/resume argv, or declare"prepare-execution", enforce it in staged policy, and returntoolAvailabilityEnforced: true. OpenClaw disables native tools for runtime caps like crontoolsAllowand fails closed when the declared enforcement path is incomplete.
For a full authoring guide, see CLI backend plugins.
Exclusive slots
| Method | What it registers |
|---|---|
api.registerContextEngine(id, factory) | Context engine (one active at a time). Lifecycle callbacks receive runtimeSettings when the host can provide model/provider/mode diagnostics; older strict engines are retried without that key. |
api.registerMemoryCapability(capability) | Unified memory capability |
Deprecated memory embedding adapters
| Method | What it registers |
|---|---|
api.registerMemoryEmbeddingProvider(adapter) | Memory embedding adapter for the active plugin |
registerMemoryCapabilityis the exclusive memory-plugin API.registerMemoryCapabilitymay also exposepublicArtifacts.listArtifacts(...)for host-managed exports. Companion plugins that enumerate those declared artifacts still uselistActiveMemoryPublicArtifacts(...)from the retainedopenclaw/plugin-sdk/memory-host-corefacade until a focused public consumer API exists; they must not reach into another plugin's private layout.MemoryFlushPlan.modelcan pin the flush turn to an exactprovider/modelreference, such asollama/qwen3:8b, without inheriting the active fallback chain.registerMemoryEmbeddingProvideris deprecated. New embedding providers should useapi.registerEmbeddingProvider(...)andcontracts.embeddingProviders.- Existing memory-specific providers continue to work during the migration window, but plugin inspection reports this as compatibility debt for non-bundled plugins.
Events and lifecycle
| Method | What it does |
|---|---|
api.on(hookName, handler, opts?) | Typed lifecycle hook |
api.onConversationBindingResolved(handler) | Conversation binding callback |
See Plugin hooks for examples, common hook names, and guard semantics.
Hook decision semantics
before_install is a plugin-runtime lifecycle hook, not the operator install policy surface. Use security.installPolicy when an allow/block decision must cover CLI and Gateway-backed install or update paths.
before_tool_call: a return of{ block: true }ends processing. Once a handler produces this value, all handlers with lower priority are bypassed.before_tool_call: returning{ block: false }indicates no decision was made, identical to leaving outblock, and does not act as an override.before_install: a return of{ block: true }halts processing. After any handler returns this, lower-priority handlers are omitted.before_install: returning{ block: false }signals no decision, equivalent to omittingblock, and is not considered an override.reply_dispatch: a return of{ handled: true, ... }is final. Once a handler claims the dispatch, lower-priority handlers and the default model dispatch path are excluded.message_sending: returning{ cancel: true }terminates processing. After any handler sets it, lower-priority handlers are not executed.message_sending: returning{ cancel: false }means no decision was reached, the same as not includingcancel, and does not override anything.message_received: when inbound thread or topic routing is required, use the typedthreadIdfield. Reservemetadatafor extras tied to a specific channel.message_sending: rely on typed routing fieldsreplyToIdandthreadIdfirst, then fall back to channel-specificmetadata.gateway_start: for gateway-owned startup state, usectx.config,ctx.workspaceDir, andctx.getCron?.()instead of internalgateway:startuphooks. The cron system may still be loading at this point.cron_reconciled: after startup or a scheduler reload, rebuild a full external cron projection. This includesreasonand the currentenabledstate, along withenabled: false, whilectx.getCron?.()provides the exact reconciled scheduler. Passctx.abortSignalto durable projection work; it aborts when that scheduler snapshot becomes stale or the Gateway closes.cron_changed: monitor lifecycle changes in gateway-owned cron. Eventsscheduledandremovedserve as post-commit reconciliation hints, not as an ordered delta log. A scheduled event'sevent.nextRunAtMsis missing when the job has no next wake; a removed event still contains the deleted job snapshot.
External wake schedulers should debounce or merge cron_changed events,
then re-read the full durable view from the scheduler most recently captured by
cron_reconciled. Do not adopt the scheduler from a cron_changed context: a
detached hint from an older scheduler can overlap with a later reload.
Use cron_reconciled as the full snapshot trigger for durable state loaded at
Gateway startup or scheduler replacement. It is not replayed during a plugin-only
hot reload. Observation handlers run in parallel, and fire-and-forget
dispatches may overlap, so consumers must not rely on event completion order.
Keep OpenClaw as the source of truth for due checks and execution.
For a single-flight adapter with durable replacement, retry/backoff, and clean shutdown, see Safe external cron projection.
API object fields
| Field | Type | Description |
|---|---|---|
api.id | string | Unique identifier for the plugin |
api.name | string | Human readable name |
api.version | string? | Plugin version, not required |
api.description | string? | Plugin description, not required |
api.source | string | Filesystem path to the plugin source |
api.rootDir | string? | Root directory of the plugin, not required |
api.config | OpenClawConfig | Active configuration snapshot, using the in memory runtime snapshot if one exists |
api.pluginConfig | Record<string, unknown> | Plugin specific configuration read from plugins.entries.<id>.config |
api.runtime | PluginRuntime | Runtime helpers |
api.logger | PluginLogger | Scoped logger, accepting debug, info, warn, error |
api.registrationMode | PluginRegistrationMode | Current load mode; "setup-runtime" represents the lightweight startup and setup window before full entry |
api.resolvePath(input) | (string) => string | Resolve a path relative to the plugin root |
Internal module convention
Inside your plugin, rely on 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)
Warning
Do not import your own plugin using
openclaw/plugin-sdk/<your-plugin>from production code. Internal imports should go through./api.tsor./runtime-api.ts. The SDK path is only the external contract.
Facade loaded bundled plugin public surfaces (api.ts, runtime-api.ts,
index.ts, setup-entry.ts, and similar public entry files) prefer the
active runtime configuration snapshot while OpenClaw is running. When no
runtime snapshot exists, they fall back to the resolved config file on disk.
Packaged bundled plugin facades must be loaded through OpenClaw's plugin
facade loaders; direct imports from dist/extensions/... bypass the manifest
and runtime sidecar checks that packaged installs use for plugin owned code.
Provider plugins can expose a narrow plugin local contract barrel when a helper is intentionally provider specific and does not belong in a generic SDK subpath yet. Bundled examples:
- Anthropic: public
api.ts/contract-api.tsseam for Claude beta header andservice_tierstream helpers. @openclaw/openai-provider:api.tsexports provider builders, default model helpers, and realtime provider builders.@openclaw/openrouter-provider:api.tsexports the provider builder plus onboarding and config helpers.
Warning
Extension production code should also avoid
openclaw/plugin-sdk/<other-plugin>imports. If a helper is genuinely shared, promote it to a neutral SDK subpath likeopenclaw/plugin-sdk/speech,.../provider-model-shared, or another capability oriented surface instead of coupling two plugins together.
Related
-
Entry points, Options for
definePluginEntryanddefineChannelPluginEntry. -
Runtime helpers, Complete
api.runtimenamespace reference. -
Setup and config, Packaging, manifests, and configuration schemas.
-
Testing, Test utilities and lint rules.
-
SDK migration, Migrating away from deprecated surfaces.
-
Plugin internals, In-depth structure and functionality model.