Skip to main content
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.
This page is for plugin authors using openclaw/plugin-sdk/* inside OpenClaw. For external apps, scripts, dashboards, CI jobs, and IDE extensions that want to run agents through the Gateway, use Gateway integrations for external apps instead.
Looking for a how-to guide instead? Start with Building plugins. Use 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

Always import from a specific subpath:
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. For channel config, publish the channel-owned JSON Schema through openclaw.plugin.json#channelConfigs. The plugin-sdk/channel-config-schema subpath is for shared schema primitives and the generic builder. OpenClaw’s bundled plugins use plugin-sdk/bundled-channel-config-schema for retained bundled-channel schemas. Deprecated compatibility exports remain on plugin-sdk/channel-config-schema-legacy; neither bundled schema subpath is a pattern for new plugins.
Do not import provider- or channel-branded convenience seams (for example openclaw/plugin-sdk/slack, .../discord, .../signal, .../whatsapp). Bundled plugins compose generic SDK subpaths inside their own api.ts / runtime-api.ts barrels; core consumers should either use those plugin-local barrels or add a narrow generic SDK contract when a need is truly cross-channel.A small set of bundled-plugin helper seams still appear in the generated export map when they have tracked owner usage. They exist for bundled-plugin maintenance only and are not recommended import paths for new third-party plugins.openclaw/plugin-sdk/discord and openclaw/plugin-sdk/telegram-account are also kept 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 exposed as a set of narrow subpaths grouped by area (plugin entry, channel, provider, auth, runtime, capability, memory, and reserved bundled-plugin helpers). For the full catalog — grouped and linked — see Plugin SDK subpaths. The compiler entrypoint inventory lives in scripts/lib/plugin-sdk-entrypoints.json; package exports are generated from the public subset after subtracting repo-local test/internal subpaths listed in scripts/lib/plugin-sdk-private-local-only-subpaths.json. 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 receives an OpenClawPluginApi object with these methods:

Capability registration

MethodWhat 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
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 must also declare their id in contracts.workerProviders. Core persists durable intent before provision(profile, operationId). Providers validate settings before external allocation and throw WorkerProviderError for permanent profile rejection. provision must adopt the same lease when the operation id repeats. Core persists the validated profile settings with the lease and supplies that snapshot to destroy({ leaseId, profile }), which must be idempotent, and inspect({ leaseId, profile }), which returns active, destroyed, or unknown. This lets providers route lifecycle calls after a gateway restart or named-profile removal. SSH endpoints use a SecretRef for keyRef, never inline key material, and include a hostKey from trusted provisioning output as exactly algorithm base64, without a hostname or comment. Core pins hostKey and never trusts a key from the first connection. 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. 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.
MethodWhat 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:
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.allowCommands 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

MethodWhat 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.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

Requester-scoped MCP connections

Keep the MCP server identity static (name, tool filter) in mcp.servers or a bundle manifest. Optionally register a connection resolver so each trusted message requester gets their own transport:
Contract notes:
  • Resolver context carries trusted host identity only (requesterSenderId, optional agentAccountId / messageChannel). Future trusted fields (for example cron/subagent user context) can be added additively.
  • One plugin owns one server name: a duplicate registerMcpServerConnectionResolver for the same serverName from 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.
  • resolve is 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 null result 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 headers are 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. 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.
MethodContract 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:
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 equivalent flat methods remain available as deprecated compatibility aliases for existing plugins. Do not add new plugin code that calls 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. scheduleSessionTurn(...) is a session-scoped convenience over the Gateway Cron scheduler. Cron owns timing and creates the background task record when the turn runs; the Plugin SDK only constrains the target session, plugin-owned naming, and cleanup. Use api.runtime.tasks.managedFlows inside the scheduled turn when the work itself needs durable multi-step Task Flow state. The contracts intentionally split authority:
  • External plugins can own session extensions, UI descriptors, commands, tool metadata, next-turn injections, and normal hooks.
  • Trusted tool policies run before ordinary before_tool_call hooks and are host-trusted. Bundled policies run first; installed-plugin policies require explicit enablement plus their local ids in contracts.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=false disables prompt-mutating hooks including agent_turn_prepare, before_prompt_build, heartbeat_prompt_contribution, prompt fields from legacy before_agent_start, and enqueueNextTurnInjection.
Examples of non-Plan consumers:
Plugin archetypeHooks used
Approval workflowSession extension, command continuation, next-turn injection, UI descriptor
Budget/workspace policy gateTrusted tool policy, tool metadata, session projection
Background lifecycle monitorRuntime lifecycle cleanup, agent event subscription, session scheduler ownership/cleanup, heartbeat prompt contribution, UI descriptor
Setup or onboarding wizardSession extension, scoped commands, Control UI descriptor
Reserved core admin namespaces (config.*, exec.approvals.*, wizard.*, update.*) always stay operator.admin, even if a plugin tries to assign a narrower gateway method scope. Prefer plugin-specific prefixes for plugin-owned methods.
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.
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 registrar
  • descriptors: parse-time command descriptors used for CLI help, routing, and lazy plugin CLI registration
  • parentPath: 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. 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.
Nested commands receive the resolved parent command as program:
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 id becomes the provider prefix in model refs like my-cli/gpt-5.
  • The backend config uses the same shape as agents.defaults.cliBackends.<id>.
  • User config still wins. OpenClaw merges agents.defaults.cliBackends.<id> over the plugin default before running the CLI.
  • Use normalizeConfig when a backend needs compatibility rewrites after merge (for example normalizing old flag shapes).
  • Use resolveExecutionArgs for request-scoped argv rewrites that belong to the CLI dialect, such as mapping OpenClaw thinking levels to a native effort flag. The hook receives ctx.executionMode; use "side-question" to add backend-native isolation flags for ephemeral /btw calls. If those flags reliably disable native tools for an otherwise always-on CLI, declare sideQuestionToolMode: "disabled" too.
  • Use prepareExecution for backend-owned launch environment or temporary auth/config bridges. Its ctx.contextTokenBudget is the effective token limit selected for the run, so native-compaction backends can align their own threshold without provider-specific core branches.
  • Backends that can disable all native tools for a specific run may declare nativeToolMode: "selectable". Restricted calls pass an empty ctx.toolAvailability.native tuple plus an exact host-isolated MCP allowlist; resolveExecutionArgs must enforce both on the final fresh or resume argv. OpenClaw fails closed if the backend cannot do so.
For an end-to-end authoring guide, see CLI backend plugins.

Exclusive slots

MethodWhat 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
api.registerMemoryPromptSection(builder)Memory prompt section builder
api.registerMemoryFlushPlan(resolver)Memory flush plan resolver
api.registerMemoryRuntime(runtime)Memory runtime adapter

Deprecated memory embedding adapters

MethodWhat it registers
api.registerMemoryEmbeddingProvider(adapter)Memory embedding adapter for the active plugin
  • registerMemoryCapability is the preferred exclusive memory-plugin API.
  • registerMemoryCapability may also expose publicArtifacts.listArtifacts(...) so companion plugins can consume exported memory artifacts through openclaw/plugin-sdk/memory-host-core instead of reaching into a specific memory plugin’s private layout.
  • registerMemoryPromptSection, registerMemoryFlushPlan, and registerMemoryRuntime are legacy-compatible exclusive memory-plugin APIs.
  • MemoryFlushPlan.model can pin the flush turn to an exact provider/model reference, such as ollama/qwen3:8b, without inheriting the active fallback chain.
  • registerMemoryEmbeddingProvider is deprecated. New embedding providers should use api.registerEmbeddingProvider(...) and contracts.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

MethodWhat 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: 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 omitting block), 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 omitting block), not as an override.
  • reply_dispatch: returning { handled: true, ... } is terminal. Once any handler claims dispatch, lower-priority handlers and the default model dispatch path are skipped.
  • 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 omitting cancel), not as an override.
  • message_received: use the typed threadId field when you need inbound thread/topic routing. Keep metadata for channel-specific extras.
  • message_sending: use typed replyToId / threadId routing fields before falling back to channel-specific metadata.
  • gateway_start: use ctx.config, ctx.workspaceDir, and ctx.getCron?.() for gateway-owned startup state instead of relying on internal gateway:startup hooks. Cron may still be loading at this point.
  • cron_reconciled: rebuild a full external cron projection after startup or scheduler reload. It includes reason and the effective enabled state, including enabled: false, while ctx.getCron?.() returns the exact reconciled scheduler. Pass ctx.abortSignal into durable projection work; it aborts when that scheduler snapshot is superseded or the Gateway closes.
  • cron_changed: observe gateway-owned cron lifecycle changes. scheduled and removed events are post-commit reconciliation hints, not an ordered delta log. A scheduled event’s event.nextRunAtMs is absent 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. Do not 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

FieldTypeDescription
api.idstringPlugin id
api.namestringDisplay name
api.versionstring?Plugin version (optional)
api.descriptionstring?Plugin description (optional)
api.sourcestringPlugin source path
api.rootDirstring?Plugin root directory (optional)
api.configOpenClawConfigCurrent config snapshot (active in-memory runtime snapshot when available)
api.pluginConfigRecord<string, unknown>Plugin-specific config from plugins.entries.<id>.config
api.runtimePluginRuntimeRuntime helpers
api.loggerPluginLoggerScoped logger (debug, info, warn, error)
api.registrationModePluginRegistrationModeCurrent load mode; "setup-runtime" is the lightweight pre-full-entry startup/setup window
api.resolvePath(input)(string) => stringResolve path relative to plugin root

Internal module convention

Within your plugin, use local barrel files for internal imports:
Never import your own plugin through openclaw/plugin-sdk/<your-plugin> 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) 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. Packaged bundled plugin facades should 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.ts seam for Claude beta-header and service_tier stream helpers.
  • @openclaw/openai-provider: api.ts exports provider builders, default-model helpers, and realtime provider builders.
  • @openclaw/openrouter-provider: api.ts exports the provider builder plus onboarding/config helpers.
Extension production code should also avoid openclaw/plugin-sdk/<other-plugin> imports. If a helper is truly shared, promote it to a neutral SDK subpath such as openclaw/plugin-sdk/speech, .../provider-model-shared, or another capability-oriented surface instead of coupling two plugins together.

Entry points

definePluginEntry and defineChannelPluginEntry options.

Runtime helpers

Full api.runtime namespace reference.

Setup and config

Packaging, manifests, and config schemas.

Testing

Test utilities and lint rules.

SDK migration

Migrating from deprecated surfaces.

Plugin internals

Deep architecture and capability model.