Plugin SDK Overview: Imports and Registration Reference
Reference for plugin authors on SDK imports and registration options. Covers import conventions, registration API, and architecture for building plugins in OpenClaw.
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 the typed interface that connects plugins to the core system. This page serves as the reference for which imports to use and what registration options exist.
Note
This page targets plugin authors working with
openclaw/plugin-sdk/*inside OpenClaw. If you are building external applications, scripts, dashboards, CI jobs, or IDE extensions that need to drive agents through the Gateway, refer to Gateway integrations for external apps instead.
Tip
Prefer a practical walkthrough? Begin with Building plugins. For specific use cases, turn to Channel plugins for 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
Imports must always target a specific subpath:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
Every subpath is a compact, self-contained module. This design keeps startup time low and sidesteps circular dependency problems. When you need channel-specific entry or build helpers, reach for openclaw/plugin-sdk/channel-core; reserve openclaw/plugin-sdk/core for the wider umbrella surface and shared utilities such as buildChannelConfigSchema.
For channel configuration, expose the channel-owned JSON Schema via openclaw.plugin.json#channelConfigs. The plugin-sdk/channel-config-schema subpath holds shared schema primitives and the generic builder. OpenClaw's bundled plugins rely on plugin-sdk/bundled-channel-config-schema for retained bundled-channel schemas. That bundled schema subpath is not meant as a template for new plugins.
Warning
Avoid importing provider- or channel-branded convenience seams (examples include
openclaw/plugin-sdk/slack,.../discord,.../signal,api.ts/runtime-api.tsbarrels; core consumers should either rely on those plugin-local barrels or introduce a narrow generic SDK contract when a genuine cross-channel need arises.A limited set of bundled-plugin helper seams still show up 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 preserved as deprecated compatibility facades for tracked owner usage. Do not copy those import paths into new plugins; instead use injected runtime helpers and generic channel SDK subpaths.
Subpath reference
The plugin SDK is organized as a collection of narrow subpaths grouped by domain (plugin entry, channel, provider, auth, runtime, capability, memory, and reserved bundled-plugin helpers). For the complete catalog, with grouping and links, see Plugin SDK subpaths.
The compiler entrypoint inventory is located in
scripts/lib/plugin-sdk-entrypoints.json; typed public exports omit the
internal subpaths listed in
scripts/lib/plugin-sdk-private-local-only-subpaths.json. Production entries
on that list keep JavaScript-only host runtime exports for separately
published official plugins, while test-only entries stay unexported. Run
pnpm plugin-sdk:surface to audit the public export count. Deprecated public
subpaths that are old enough 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 gets an OpenClawPluginApi object offering these
methods:
Plugins that expose 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 another 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 also required to declare their id in contracts.workerProviders.
Before provision(profile, operationId) happens, core persists durable intent. Providers validate settings prior to external allocation, and for permanent profile rejection they throw WorkerProviderError. When an operation id is repeated, provision must take on the same lease. If a provider's provisioning can legitimately go beyond core's five-minute default, it may return a positive millisecond budget from resolveProvisionTimeoutMs(profile); that bound should cover acquisition, provider-owned setup, and cleanup.
The validated profile settings are persisted by core together with the lease, and that snapshot is supplied to destroy({ leaseId, profile }), which has to be idempotent, as well as inspect({ leaseId, profile }), which yields active, destroyed, or unknown. This allows providers to route lifecycle calls following a gateway restart or named-profile removal. SSH endpoints rely on a SecretRef for keyRef, never inline key material, and must include a hostKey taken from trusted provisioning output exactly as algorithm base64, without a hostname or comment. Core pins hostKey and refuses to trust a key from the initial connection. Providers may additionally return up to 10 ordered, unique fallbackPorts (integer ports ranging from 1 through 65535, with the primary port excluded); core validates and persists those advertised candidates for idempotent probes, content-addressed transfers, receipt/lock-guarded artifact installation, convergent managed-worktree mirroring, and tunnel reconnects. Ambiguous unguarded stateful commands fail closed and are never replayed across candidates. A lease can set sharedHost: true when the SSH account also owns unrelated processes; core then avoids host-wide process freezing during workspace reconciliation. If false is omitted, a dedicated worker host is assumed. Active inspection repeats this fact so core can reconcile provider-owned isolation for leases persisted before the field existed; tunnel startup waits for that first authoritative inspection. A provider that mints 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.
WorkerLease.desktop is optional and takes the shape { protocol: "rfb"; port: number; passwordFilePath?: string; apps?: WorkerDesktopApp[] }; passwordFilePath, when present, must be absolute. Providers report this warm-time capability from provision; it cannot be retrofitted onto a live lease. When needed, the Gateway reads the password file over the provider's SSH endpoint and never persists the password. WorkerDesktopApp is a closed union: { id: "browser"; executablePath: string; cdpPort: number } or { id: "terminal"; executablePath: string }. App ids must be unique, executable paths must be absolute, browser CDP ports must be integers from 1 through 65535, and the list accepts at most eight entries. Core rejects unknown ids and fields.
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 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(...) stay on the existing per-file batching contract unless their runtime explicitly sets sourceWideBatchEmbed: true. That opt-in lets the memory host 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 |
Computer Use providers use registerComputerUseProvider(api, provider) from openclaw/plugin-sdk/computer-use. It registers the shared screen.snapshot/computer.act node-host envelope once while the provider keeps its driver, frame, availability, and execution lifecycle local.
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.
Commands may also declare a bounded client presentation action for parsed no-argument invocations:
clientPresentation: {
when: "no-arguments",
action: { kind: "device-pairing" },
}
The action union is closed and intentionally does not accept routes, callbacks, URLs, or arbitrary client data. Supporting clients handle the action only when they can complete it; otherwise the command follows its normal remote path. This metadata expresses presentation intent, not authorization: the Gateway remains authoritative for every RPC the client flow performs.
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 can carry openclaw_main, codex_app_server,
cli_backend, acp_backend, or subagent. The alias pi_main is still supported but deprecated, pointing back to
openclaw_main. When you want guidance applied across every surface intentionally, leave out surfaces. An empty
surfaces array must never be passed; it gets rejected so that an accidental loss of scope cannot turn into global prompt text.
Prompt surfaces other than the native Codex app server follow looser rules. Only guidance that is explicitly scoped to codex_app_server gets promoted into that higher-priority lane. Legacy string guidance and structured guidance without a scope stay available to non-Codex prompt surfaces so nothing breaks.
Commands for node hosts execute on the connected node host, not within the Gateway process. When agentTool is supplied, the node publishes a descriptor after a successful Gateway connect; the Gateway makes it visible to agent runs only while that node is connected and only when the descriptor's command falls within the node's approved command surface. To place a non-dangerous command on the default node command allowlist, set agentTool.defaultPlatforms; otherwise you need explicit gateway.nodes.commands.allow or a node-invoke policy. agentTool.name
has to be provider-safe: it must begin with a letter, contain only letters, digits,
underscores, or hyphens, and be no longer than 64 characters. Node tools backed by MCP can attach agentTool.mcp metadata so catalog and tool-search surfaces can display the remote MCP server/tool identity, though execution still routes 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
Webhook routes that send an acknowledgement before processing wraps up need to shift that detached work onto 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)}`);
});
While the HTTP request is still admitted, call runDetachedWebhookWork(...) synchronously. The helper reserves a separate root immediately, then launches the callback in the next microtask so the request handler can write its acknowledgement first. Whatever the callback returns, the returned promise adopts; rejection handling stays with the caller. This makes post-ack queue work accepted and forces restart or suspension drains to wait for it. Handlers that await all processing before returning can skip this helper.
Requester-scoped MCP connections
Keep the MCP server identity fixed (name, tool filter) in mcp.servers, a
native plugin's mcpServers manifest field, or a bundle manifest. You may register a connection resolver so each trusted message requester receives 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:
- The resolver context exposes only the trusted host identity (
requesterSenderId, with optionalagentAccountId/messageChannel). Additional trusted fields, such as cron or subagent user context, can be introduced later without breaking existing behavior. - Each server name belongs to a single plugin. If another plugin attempts a
duplicate
registerMcpServerConnectionResolverfor the sameserverName, the system rejects it with an error diagnostic, and the first registration takes precedence. This ensures connection ownership is independent of plugin load order. - Tool names come from the complete declared server set, so partial resolution never alters safe server names between requesters or turns. The core does not check that different requester endpoints expose identical tool schemas; a resolver must direct every requester to the same logical service, otherwise tool schemas and prompt-cache stability will differ per requester.
- Runs lacking a trusted
requesterSenderId(cron, subagent, heartbeat, public gateway) never instantiate requester-scoped servers. No shared fallback connection exists. resolveis capped at 10 seconds per server. A timeout or thrown error excludes that server from the run without breaking static MCP.- Resolved connections are rechecked at most every 5 minutes per requester.
Rotation rebuilds the transport with fresh credentials, and a
nulloutcome revokes it, disposing the cached runtime even mid-session. As a result, a revoked or rotated credential may remain active for up to 5 minutes. - Resolved
headersvalues are never logged or persisted. The core keeps only a transient in-memory keyed digest, a process-local HMAC, to detect credential rotation, and it registers resolved header or URL credential values with the log and debug-capture redaction registry. - Requester-scoped servers do not generate MCP App views. A view persists beyond the requester-authenticated run, and the gateway view boundary lacks requester identity, so app previews remain fail-closed for these servers. Tool results are unaffected.
- Static servers without a resolver retain the existing session-scoped lifecycle.
- Harness delivery rule: requester-scoped servers never appear in
harness-native MCP client configuration, including 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 plus bundle tools (static and scoped).
- Codex app-server: dynamic tools via
materializeRequesterScopedMcpToolsForHarnessRun(scoped-only; static servers remain on Codex's native MCP client).
- Scoped tool specs remain session-stable after the first successful resolve in that session, so shared-thread harnesses like 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 per call rather than capturing a single
global path during registration. If an agent id is required but missing in a
multi-agent operation, fail closed instead of selecting an arbitrary agent.
Use registerMemoryPromptPreparation(...) when prompt text depends on async plugin state. The callback
executes 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 passes 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 are the SDK seams for plugins that need to participate in the host lifecycle rather than only adding a provider, channel, or tool. They 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(...) | Plugin-owned, JSON-compatible session state projected through Gateway sessions |
api.session.workflow.enqueueNextTurnInjection(...) | Durable exactly-once context injected into the next agent turn for one session |
api.registerTrustedToolPolicy(...) | Manifest-gated trusted pre-plugin tool policy that can block or rewrite tool params |
api.registerToolMetadata(...) | Tool catalog display metadata without changing 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/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 on terminal run lifecycle |
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'
tab descriptors are advertised 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"],
});
When writing new plugin code, prefer the grouped namespaces:
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(...)
For existing plugins, the corresponding flat methods still work, but they are now deprecated compatibility aliases. Avoid calling 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 directly in any new plugin code.
scheduleSessionTurn(...) provides a session-scoped convenience layer over the Gateway Cron scheduler. Cron handles the timing and creates the background task record when the turn executes; the Plugin SDK only restricts the target session, plugin-owned naming, and cleanup. Inside the scheduled turn, use api.runtime.tasks.managedFlows when the work requires durable multi-step Task Flow state.
The contracts split authority on purpose:
- External plugins may own session extensions, UI descriptors, commands, tool metadata, next-turn injections, and normal hooks.
- Trusted tool policies run before ordinary
before_tool_callhooks and are host-trusted. Bundled policies run first; installed-plugin policies need explicit enablement plus their local ids incontracts.trustedToolPolicies, and run next in plugin-load order. Policy ids are scoped to the registering plugin. - Reserved command ownership is bundled-only. External plugins should use their own command names or aliases.
allowPromptInjection=falsedisables prompt-mutating hooks includingagent_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 stayoperator.admin, even if a plugin tries 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 use api.registerAgentToolResultMiddleware(...) when they need to rewrite a tool result after execution and before the runtime feeds that result back into the model. This is the trusted runtime-neutral seam for async output reducers such as tokenjuice.
Plugins must declare contracts.agentToolResultMiddleware for each targeted runtime, for example ["openclaw", "codex"]. Installed plugins without that contract, or without explicit enablement, cannot register this middleware; keep normal OpenClaw plugin hooks for work that does not need pre-model tool-result timing. The old embedded-runner-only extension factory registration path has been removed.
Gateway discovery registration
api.registerGatewayDiscoveryService(...) lets a plugin 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 calls 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 is a routing hint; Gateway auth and TLS pinning still own 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 is a small wrapper around api.registerCli(..., { parentPath: ["nodes"] }) and makes commands such as openclaw nodes canvas explicit plugin-owned node features.
To keep a plugin command lazy-loaded in the default root CLI path, supply descriptors entries that cover every top-level command root that registrar exposes.
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,
},
],
},
);
A root descriptor may additionally declare machineOutput({ argv, stdoutIsTTY }) when the command reserves stdout for JSON, JSONL, or another machine-readable format and does not rely solely on a literal --json flag. OpenClaw runs this resolver before plugin activation, allowing startup diagnostics to go to stderr. The resolver must be synchronous, pure, and dependency-light: it should inspect only the provided raw argv and the stdout TTY state. Reuse the same resolver in lightweight CLI metadata and full registration so discovery and execution stay consistent. When the resolver needs command-path tokens, pull in getRootOptionAwareCommandPath from openclaw/plugin-sdk/cli-argv; it accepts supported root options either before or after the command root. machineOutput is root metadata, and nested descriptors cannot use it because their owning root must be active before they become visible.
Nested commands get 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 alone only when lazy root CLI registration is unnecessary. 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 in model refs likemy-cli/gpt-5. - The backend
configis the authoritative command adapter: argv, environment, parser, session, image, and reliability behavior live in plugin code. - Users pick the backend through model refs or model-scoped
agentRuntime.id;openclaw.jsondoes not rewrite the adapter. - Use
normalizeConfigwhen registered static fields need a runtime-aware normalization pass. - Use
resolveExecutionArgsfor request-scoped argv rewrites that belong to the CLI dialect, such as mapping OpenClaw thinking levels to a native effort flag. The hook receivesctx.executionMode; use"side-question"to add backend-native isolation flags for ephemeral/btwcalls. If those flags reliably disable native tools for an otherwise always-on CLI, declaresideQuestionToolMode: "disabled"too. - Use
prepareExecutionfor backend-owned launch environment or temporary auth/config bridges. Itsctx.contextTokenBudgetis the effective token limit selected for the run, so native-compaction backends can align their own threshold without provider-specific core branches. It also receives the core-preparedctx.envwhen backend staging must extend bundled MCP settings. - Backends that can disable all native tools for a specific run may 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 such as crontoolsAllowand fails closed when the declared enforcement path is incomplete.
For an end-to-end authoring guide, see CLI backend plugins.
Exclusive slots
| Method | What it registers |
|---|---|
api.registerContextEngine(id, factory) | Context engine (one active at a time). Use info.acceptedHostParams to restrict accepted host-added lifecycle fields; undeclared engines receive all current host fields. |
api.registerMemoryCapability(capability) | Unified memory capability |
To participate in durable admitted turns, context engines must declare currentTurnFence: "before-current-turn-entry-v1" and turnAdvancementIdempotency: "atomic-idempotent-v1" under info.transcriptSemantics, then implement commitTurn(...) as an atomic, idempotent write keyed by advancementKey. OpenClaw supplies only the inclusive accepted turn, from its admitted user entry through its terminal entry; use the readSessionTranscriptVisibleMessageDelta(...) cursor API to bootstrap or rebuild earlier history. Without the full contract, OpenClaw uses the legacy context path for the whole logical turn and its retries, leaves the configured engine unchanged, and tries that engine again on the next logical turn.
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.- A memory runtime that can return session-transcript hits should implement
runtime.authorizeSearchHits(...). The host calls this hook before raw search hits reach caller-visible surfaces and supplies the requesting agent, session key, and sandbox state. Return only hits the requester may observe. If the hook is absent, OpenClaw fails closed by withholding session-source hits while retaining ordinary memory hits. Keep transcript identity and visibility policy in the owning memory plugin; callers must not infer authorization from paths or duplicate plugin-specific rules. 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 |
Head to Plugin hooks for examples, common hook names, and guard semantics.
Hook decision semantics
before_install belongs to the plugin runtime lifecycle, not the operator install
policy surface. Reach for security.installPolicy when an allow/warn/block decision needs to
span both CLI and Gateway-backed install or update flows.
before_tool_call: a return of{ block: true }ends processing. Once any handler does this, lower-priority handlers get skipped.before_tool_call: a return of{ block: false }counts as no decision (identical to leaving outblock), not as an override.before_install: a return of{ block: true }ends processing. Once any handler does this, lower-priority handlers get skipped.before_install: a return of{ block: false }counts as no decision (identical to leaving outblock), not as an override.reply_dispatch: a return of{ handled: true, ... }ends processing. Once any handler claims dispatch, lower-priority handlers and the default model dispatch path get skipped.message_sending: a return of{ cancel: true }ends processing. Once any handler does this, lower-priority handlers get skipped.message_sending: a return of{ cancel: false }counts as no decision (identical to leaving outcancel), not as an override.message_received: for inbound thread/topic routing, rely on the typedthreadIdfield. Reservemetadatafor channel-specific extras.message_sending: prefer typedreplyToId/threadIdrouting fields before dropping back to channel-specificmetadata.gateway_start: gateway-owned startup state should come fromctx.config,ctx.workspaceDir, andctx.getCron?.()rather than internalgateway:startuphooks. Cron may still be loading at this point.cron_reconciled: after startup or scheduler reload, reconstruct a full external cron projection. It coversreasonand the effectiveenabledstate, includingenabled: false, whilectx.getCron?.()yields the exact reconciled scheduler. Feedctx.abortSignalinto durable projection work; it aborts when that scheduler snapshot is superseded or the Gateway closes.cron_changed: watch gateway-owned cron lifecycle changes.scheduledandremovedevents are post-commit reconciliation hints, not an ordered delta log. A scheduled event'sevent.nextRunAtMsis missing when the job has no next wake; a removed event still carries the deleted job snapshot.
External wake schedulers should debounce or coalesce cron_changed events,
then reread the full durable view from the scheduler last captured by
cron_reconciled. Never adopt the scheduler from a cron_changed context: a
detached hint from an older scheduler can overlap 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 for a plugin-only
hot reload. Observation handlers run in parallel, and fire-and-forget
dispatches can overlap, so consumers must not depend 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 | Identifier for the plugin |
api.name | string | Name shown to users |
api.version | string? | Plugin version, when provided |
api.description | string? | Plugin description, when provided |
api.source | string | Filesystem location of the plugin source |
api.rootDir | string? | Root folder for the plugin, when provided |
api.config | OpenClawConfig | Current configuration snapshot, using the live in-memory runtime snapshot if one exists |
api.pluginConfig | Record<string, unknown> | Plugin-specific settings pulled from plugins.entries.<id>.config |
api.runtime | PluginRuntime | Runtime helper utilities |
api.logger | PluginLogger | Logger scoped to this plugin (debug, info, warn, error) |
api.registrationMode | PluginRegistrationMode | Active loading mode; "setup-runtime" denotes the minimal startup path where runtime is ready |
api.resolvePath(input) | (string) => string | Resolve a path relative to the plugin's root directory |
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
Production code must not import your own plugin through
openclaw/plugin-sdk/<your-plugin>. Direct internal imports should go through./api.tsor./runtime-api.ts. The SDK path serves as the external contract only.
Facade-loaded bundled plugin public surfaces (api.ts, runtime-api.ts,
index.ts, setup-entry.ts, and other public entry files) favor the
active runtime config snapshot when OpenClaw is already running. In the absence
of a runtime snapshot, they fall back to the resolved config file stored on disk.
Packaged bundled plugin facades should be loaded through OpenClaw's plugin
facade loaders; importing directly from dist/extensions/... skips the manifest
and runtime sidecar checks that packaged installs rely on for plugin-owned code.
Provider plugins can expose a narrow plugin-local contract barrel when a helper is intentionally provider-specific and does not yet fit a generic SDK subpath. 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/config helpers.
Warning
Extension production code should also steer clear of
openclaw/plugin-sdk/<other-plugin>imports. When a helper is genuinely shared, move it up 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 config schemas.
-
Testing, Test utilities and lint rules.
-
SDK migration, Moving away from deprecated surfaces.
-
Plugin internals, Detailed internal structure and the underlying capability framework.