Migrating Plugins to the Modern SDK
This guide helps plugin developers move from the legacy backwards-compatibility layer to the current SDK. It details removed import paths and provides steps for updating your plugin.
Read this when
- You used api.registerEmbeddedExtensionFactory before OpenClaw 2026.4.25
- You are updating a plugin to the modern plugin architecture
- You maintain an external OpenClaw plugin
OpenClaw swapped a broad backwards-compatibility layer for a plugin architecture built on small, focused imports. If your plugin was written before that shift, this guide walks you through moving it onto the current contracts.
What changed
A handful of wide-open import surfaces used to give plugins near-universal access from one entry point:
openclaw/plugin-sdkandopenclaw/plugin-sdk/compat- re-exported dozens of helpers while the focused SDK was under construction. Both roots are now gone; import a documented subpath instead.openclaw/plugin-sdk/infra-runtime- a catch-all barrel mixing system events, heartbeat state, delivery queues, fetch/proxy helpers, file helpers, approval types, and unrelated utilities.openclaw/plugin-sdk/config-runtime- a broad config barrel kept only for its later compatibility window; direct runtime load/write helpers have been removed.openclaw/extension-api- a removed bridge that handed plugins direct access to host-side helpers like the embedded agent runner.api.registerEmbeddedExtensionFactory(...)- a removed embedded-runner-only hook that watched embedded-runner events such astool_result. Use agent tool-result middleware instead (see Migrate embedded tool-result extensions to middleware).
The root SDK, compat barrel, extension bridge, and embedded extension factory
are all removed. infra-runtime and config-runtime persist only for
their separately recorded later windows; new plugins should use focused subpaths.
Warning
Plugins importing the removed root, compat, or extension surfaces will no longer load. Follow the mappings below before upgrading.
OpenClaw does not remove or reinterpret documented plugin behavior in the same change that introduces a replacement. Breaking contract changes go through a compatibility adapter, diagnostics, docs, and a deprecation window first. That applies to SDK imports, manifest fields, setup APIs, hooks, and runtime registration behavior.
Why
- Slow startup - importing one helper loaded dozens of unrelated modules.
- Circular dependencies - broad re-exports made import cycles easy to create.
- Unclear API surface - no way to tell stable exports from internal ones.
Each openclaw/plugin-sdk/<subpath> is now a small, self-contained module with
a documented contract.
Legacy provider convenience seams for bundled channels are gone too -
channel-branded helper shortcuts were private mono-repo conveniences, not
stable plugin contracts. Use narrow generic SDK subpaths instead. Inside the
bundled plugin workspace, keep provider-owned helpers in that plugin's own
api.ts or runtime-api.ts:
- Anthropic keeps Claude-specific stream helpers in its own
api.ts/contract-api.tsseam. - OpenAI keeps provider builders, default-model helpers, and realtime provider
builders in its own
api.ts. - OpenRouter keeps provider builder and onboarding/config helpers in its own
api.ts.
Compatibility policy
External-plugin compatibility work follows this order:
- Add the new contract.
- Keep the old behavior wired through a compatibility adapter.
- Emit a diagnostic or warning naming the old path and replacement.
- Cover both paths in tests.
- Document the deprecation and migration path.
- Remove only after the announced migration window, usually in a major release.
Channel state migration declarations
Channel plugins should declare doctorContract.stateMigrations: true in
openclaw.plugin.json and export stateMigrations from their doctor-contract
artifact. Plan-based migrations can use
definePluginDoctorMigrationFromPlans(...) from
openclaw/plugin-sdk/runtime-doctor-migrations to preserve existing move, copy, preview,
and plugin-state import behavior.
The setup-entry legacyStateMigrations option and feature flag,
setupFeatures.legacyStateMigrations,
BundledChannelLegacyStateMigrationDetector, and
ChannelPlugin.lifecycle.detectLegacyStateMigrations remain supported through
one doctor-pipeline adapter for external plugins, but are deprecated. Removal
plan: remove that adapter after OpenClaw 2027.1 only when a published-plugin
reader sweep finds no remaining users.
AuthStorage SQLite migration
AuthStorage.forAgent(agentDir) is the canonical provider-keyed session SDK
facade. It persists provider-default credentials through the agent's
openclaw-agent.sqlite auth-profile rows and never creates auth.json.
AuthStorage.create(authPath) remains as a named deprecated adapter for
existing plugins. The path is used only to derive the owning agent directory;
the adapter reads and writes SQLite, not the named JSON file. Migrate to
forAgent(...) now. The path-taking form emits
AUTH_STORAGE_CREATE_DEPRECATED and is eligible for removal after
2026-10-01, provided the published-plugin reader sweep is clean.
Direct FileAuthStorageBackend imports remain available through the same
window as a SQLite-backed compatibility adapter. They emit
FILE_AUTH_STORAGE_BACKEND_DEPRECATED; replace backend construction with
AuthStorage.forAgent(agentDir). Neither deprecated path reads or writes the
legacy file.
If a manifest field is still accepted, keep using it until docs and diagnostics say otherwise. New code should prefer the documented replacement; existing plugins should not break during ordinary minor releases.
The dated compatibility registry also tracks shipped annotations that do not belong to one legacy subpath. These records use 2026-10-01 as the earliest review date; removal still requires the reader condition in the final column.
| Compatibility code | Replacement | Removal condition |
|---|---|---|
plugin-sdk-broad-runtime-barrels | Focused capability subpaths | No bundled or published imports of the seven enumerated broad barrels remain. |
plugin-sdk-provider-owned-helper-shims | Provider-local auth/model/replay/OAuth/stream APIs | Every enumerated helper is migrated in official providers and absent from published plugins. |
message-presentation-legacy-bridges | MessagePresentation and channel presentation renderers | Producers and official channel packages no longer emit or read legacy interactive replies. |
plugin-sdk-focused-compat-aliases | The focused replacement named by each @deprecated annotation | Every enumerated alias has zero bundled and published readers. |
agent-harness-terminal-result-aliases | AgentHarnessAttemptResult.terminal and visibleReplies | Harness plugins no longer read legacy terminal booleans or sourceVisibleReplies. |
official-plugin-export-aliases | Canonical Google Meet testing, presentation renderers, and host-owned Discord timeout behavior | Minimum supported official plugin packages no longer import the aliases. |
memory-host-compatibility-aliases | Canonical memory tables and prepared runtime config | Memory integrations no longer pass table overrides or call legacy loadConfig. |
plugin-runtime-api-compat-aliases | Namespaced plugin APIs and focused runtime methods | All enumerated flat API/runtime aliases have no readers. |
plugin-provider-manifest-compat-aliases | Manifest-owned kind/setup metadata and model catalog registration | Providers no longer publish runtime kind or legacy catalog hooks. |
Published channel setup compatibility
Slack, Discord, Signal, and Microsoft Teams packages released through
2026.7.1 pull in channel-specific configuration schemas from
openclaw/plugin-sdk/bundled-channel-config-schema. The Slack and Discord packages that are published also import
createLegacyCompatChannelDmPolicy and promptLegacyChannelAllowFromForAccount from
openclaw/plugin-sdk/setup-runtime.
These exports are still available, acting as deprecated runtime compatibility adapters. Plugins that are new or being republished should define their own config schemas and setup policy locally, relying on the generic primitives found in channel-config-schema and setup-runtime. Only after the minimum supported versions of published packages stop importing them can the compatibility exports be taken out.
Channel setup input field compatibility
ChannelSetupInput now permanently types only the cross-channel setup envelope. Channel-specific fields stay typed within a deprecated compatibility tier, so existing external plugins continue to compile while plugin authors shift those fields into plugin-local setup input types.
OpenClaw does not ship major releases. A registry sweep performed on 2026-07-22 examined 426 published out-of-tree channel plugins and dropped 21 fields that had no readers. Each of the 22 fields that were kept has a known published reader. Any further field is removed as soon as no published plugin reads it; the kept set gets smaller as plugin authors move to plugin-local setup input types.
That same sweep eliminated 23 legacy undeclared-adapter promotion keys lacking published dependents. Six common keys plus the setup-only rooms key remain. This set also shrinks as published plugins declare singleAccountKeysToMove.
The shared type carries no index signature. Plugin-owned keys can still appear on runtime input objects; declare them in a plugin-local intersection or narrow them via the owning plugin's setup schema.
code | owner | replacement | Removal condition |
|---|---|---|---|
plugin-sdk-channel-setup-input-fields | channel | Intersect ChannelSetupInput with a plugin-local type that declares the owning channel's fields | Delete a field when the published-plugin registry sweep has no reader |
The legacy undeclared-adapter promotion tier follows the same reader-driven policy. Declare singleAccountKeysToMove, including an empty array when the plugin needs no extra promotion keys, so the shared fallback can be retired one key at a time.
Verifying readers
- Page through
https://clawhub.ai/api/v1/packages?family=code-plugin&limit=100with eachnextCursor, and keep packages whosecategoriesincludechannels. - Add npm candidates from
npm search --json --searchlimit=1000 "openclaw channel plugin". Add source-only candidates from GitHub code searches foropenclaw/plugin-sdk/channel-setup,openclaw/plugin-sdk/setup, andopenclaw/plugin-sdk/core. - Resolve each candidate's latest published version. Run
npm pack <package>@<version> --json --pack-destination <temp-dir>, unpack it, and inspect shippeddistJavaScript and declarations for direct or destructured field reads. Download the ClawHub artifact when a package has no npm release. - Record package, version, field or promotion key, and matching file. A field or key is deletable only when no published plugin artifact reads it. Keep the reader names in the code comments beside the retained field and key lists synchronized with the sweep.
This is a source/type compatibility record only. The registry entry has
removeAfter: 2026-10-01, but setup input runtime objects and behavior are
unchanged. The date starts a review; each field remains until its published
artifact reader count is zero.
Audit the current migration queue with pnpm plugins:boundary-report:
| Flag | Effect |
|---|---|
--summary (or pnpm plugins:boundary-report:summary) | Compact counts instead of full detail. |
--json | Machine-readable report. |
--owner <id> | Filter to one compatibility owner. |
--fail-on-eligible-compat | Exit non-zero on or after a deprecated compat record's removeAfter date. |
pnpm plugins:boundary-report:ci runs with the compatibility fail flag.
Deprecated records normally have an explicit removeAfter date. A contract
tied to a version boundary instead declares a removalGate;
next-plugin-sdk-major is an approved major-version gate, not a pending owner
decision, and is never date-eligible. A record with neither field appears as
no-date and remains ineligible until its owner publishes a gate. The report
displays either the date or named gate, counts local code/doc references, lists
removal-pending records with their blockers and surface-token reader
references, and summarizes the private memory-host SDK bridge. Those reader
references are triage signals, not published-artifact proof.
Media legacy projection
The media-legacy-projection compatibility record covers the old parallel
media fields, payload builders, hook metadata aliases, and media template
names. Its approved removeAfter date is 2026-10-01 (two release trains
after the facts-first replacements shipped). Removal additionally requires a
clean published-plugin artifact sweep at that time; migrate before the date.
For channel ingress, replace singular/plural MediaPath, MediaUrl,
MediaType, MediaPaths, MediaUrls, MediaTypes,
MediaTranscribedIndexes, MediaWorkspaceDir, and MediaStaged with ordered
facts:
import { toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
const media = toInboundMediaFacts([
{ path: saved.path, url: nativeUrl, contentType: saved.contentType, messageId },
]);
const ctx = finalizeInboundContext({ Body: caption, media });
Use event.media inside the inbound_claim and message_received hooks. When remote media has not been staged locally, rely on event.originalMedia for identity and diagnostics, then wait for event.media; that state is what event.mediaStagingPending identifies. Avoid reading the deprecated singular and plural properties from event.metadata.
For CLI media models, swap {{MediaPath}}, {{MediaUrl}}, {{MediaType}}, and {{MediaDir}} for {{AttachmentPath}}, {{AttachmentUrl}}, {{AttachmentContentType}}, and {{AttachmentDir}}. Reach for {{AttachmentIndex}} when the position of an attachment matters.
For the local media read policy, bring in getAgentScopedMediaLocalRoots(...) or getAgentScopedMediaLocalRootsForSources(...) from openclaw/plugin-sdk/media-local-roots. Both the openclaw/plugin-sdk/agent-media-payload facade and its buildAgentMediaPayload(...) projection are marked as deprecated.
How to migrate
Migrate runtime config load/write helpers
Bundled plugins must stop invoking api.runtime.config.loadConfig() and api.runtime.config.writeConfigFile(...) directly. Instead, prefer configuration that has already been supplied along the active call path. Handlers that live for a long time and need the current process snapshot can turn to api.runtime.config.current(). Long-lived agent tools should fetch ctx.getRuntimeConfig() inside execute so a tool instantiated before a config write still observes the updated config.
Config writes go through the transactional helper with an explicit after-write policy:
await api.runtime.config.mutateConfigFile({
afterWrite: { mode: "auto" },
mutate(draft) {
draft.plugins ??= {};
},
});
Use afterWrite: { mode: "restart", reason: "..." } when the change demands a clean gateway restart, and afterWrite: { mode: "none", reason: "..." } only when the caller handles the follow-up itself and intentionally disables the reload planner. Mutation results carry a typed followUp summary for tests and logging; the gateway still owns applying or scheduling the restart.
loadConfig and writeConfigFile are no longer part of the plugin runtime. Bundled plugins and repo runtime code sit behind pnpm check:deprecated-api-usage and pnpm check:no-runtime-action-load-config: new production plugin usage fails outright, direct config writes fail, gateway server methods must use the request runtime snapshot, runtime channel send/action/client helpers must receive config from their boundary, and long-lived runtime modules allow zero ambient loadConfig() calls.
New plugin code should steer clear of the broad openclaw/plugin-sdk/config-runtime barrel. Pick the narrow subpath that fits the task:
| Need | Import |
|---|---|
Config types such as OpenClawConfig | openclaw/plugin-sdk/config-contracts |
| Plugin-entry config lookup | api.pluginConfig |
| Config merging | Plugin-local logic at the config boundary |
| Current runtime snapshot reads | openclaw/plugin-sdk/runtime-config-snapshot |
| Config writes | openclaw/plugin-sdk/config-mutation |
| Session store helpers | openclaw/plugin-sdk/session-store-runtime |
| Markdown table config | openclaw/plugin-sdk/markdown-table-runtime |
| Group policy runtime helpers | openclaw/plugin-sdk/runtime-group-policy |
| Secret input resolution | openclaw/plugin-sdk/secret-input-runtime |
| Model/session overrides | openclaw/plugin-sdk/model-session-runtime |
Scanner guards keep bundled plugins and their tests away from the broad barrel, so imports and mocks stay limited to the behavior they actually need. The barrel remains for external compatibility, but fresh code should not build on it.
Migrate embedded tool-result extensions to middleware
Bundled plugins must swap embedded-runner-only api.registerEmbeddedExtensionFactory(...) tool-result handlers for runtime-neutral middleware:
// OpenClaw runtime tools and Codex runtime dynamic tools (result may be
// transformed). Codex-native tool results are also relayed for observation,
// but their transformed output never reaches the model: the Codex
// PostToolUse hook contract cannot replace a native tool response.
api.registerAgentToolResultMiddleware(async (event) => {
return compactToolResult(event);
}, {
runtimes: ["openclaw", "codex"],
});
Update the plugin manifest at the same time:
{
"contracts": {
"agentToolResultMiddleware": ["openclaw", "codex"]
}
}
Installed plugins can also register tool-result middleware when explicitly enabled and every targeted runtime is listed in contracts.agentToolResultMiddleware. Registrations of installed middleware that are not declared get rejected.
Migrate approval-native handlers to capability facts
Approval-capable channel plugins surface native approval behavior via approvalCapability.nativeRuntime along with the shared runtime-context registry:
- Swap
approvalCapability.handler.loadRuntime(...)out forapprovalCapability.nativeRuntime. - Approval-related authentication and delivery should no longer rely on
legacy
plugin.auth/plugin.approvalsconnections; route them throughapprovalCapabilityinstead. - The public channel-plugin contract no longer includes
ChannelPlugin.approvals; relocate delivery, native, and render fields toapprovalCapability. plugin.authis now reserved exclusively for channel login and logout flows; the core no longer reads approval auth hooks from it.- Register runtime objects owned by the channel (clients, tokens, Bolt
apps) via
openclaw/plugin-sdk/channel-runtime-context. - Native approval handlers must not emit plugin-owned reroute notices; routed-elsewhere notices come from the core based on actual delivery results.
- When supplying
channelRuntimetocreateChannelManager(...), pass a completecreatePluginRuntime().channelsurface; partial stubs will be rejected.
Refer to Channel Plugins for the current approval capability layout.
Audit Windows wrapper fallback behavior
If your plugin relies on openclaw/plugin-sdk/windows-spawn, unresolved Windows
.cmd/.bat wrappers now fail closed unless you
explicitly provide allowShellFallback: true:
// Before
const program = applyWindowsSpawnProgramPolicy({ candidate });
// After
const program = applyWindowsSpawnProgramPolicy({
candidate,
// Only set this for trusted compatibility callers that intentionally
// accept shell-mediated fallback.
allowShellFallback: true,
});
Unless your caller intentionally depends on shell fallback, avoid setting
allowShellFallback and manage the thrown error instead.
Find deprecated imports
grep -r "plugin-sdk/compat" my-plugin/
grep -r "plugin-sdk/infra-runtime" my-plugin/
grep -r "plugin-sdk/config-runtime" my-plugin/
grep -r "openclaw/extension-api" my-plugin/
Replace with focused imports
Every export from the old surface has a dedicated modern import path:
// Before (deprecated backwards-compatibility layer)
import {
createChannelReplyPipeline,
createPluginRuntimeStore,
resolveControlCommandGate,
} from "openclaw/plugin-sdk/compat";
// After (modern focused imports)
import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
For host-side helpers, rely on the injected plugin runtime rather than direct imports:
// Before (deprecated extension-api bridge)
import { runEmbeddedAgent } from "openclaw/extension-api";
const result = await runEmbeddedAgent({ sessionId, prompt });
// After (injected runtime)
const result = await api.runtime.agent.runEmbeddedAgent({ sessionId, prompt });
The same approach applies to other legacy bridge helpers:
| Old import | Modern equivalent |
|---|---|
resolveAgentDir | api.runtime.agent.resolveAgentDir |
resolveAgentWorkspaceDir | api.runtime.agent.resolveAgentWorkspaceDir |
resolveAgentIdentity | api.runtime.agent.resolveAgentIdentity |
resolveThinkingDefault | api.runtime.agent.resolveThinkingDefault |
resolveAgentTimeoutMs | api.runtime.agent.resolveAgentTimeoutMs |
ensureAgentWorkspace | api.runtime.agent.ensureAgentWorkspace |
| session store helpers | api.runtime.agent.session.* |
Replace broad infra-runtime imports
openclaw/plugin-sdk/infra-runtime remains available for external
compatibility, yet new code should adopt the supported surface it
actually requires:
| Need | Replacement |
|---|---|
| New system event producers | api.runtime.system.enqueueSystemEvent |
| Heartbeat wake, event, and visibility helpers | openclaw/plugin-sdk/heartbeat-runtime |
| Pending delivery queue drain | openclaw/plugin-sdk/delivery-queue-runtime |
| Channel activity telemetry | openclaw/plugin-sdk/channel-activity-runtime |
| In-memory and persistent-backed dedupe caches | openclaw/plugin-sdk/dedupe-runtime |
| Safe local-file/media path helpers | openclaw/plugin-sdk/file-access-runtime |
| Dispatcher-aware fetch | openclaw/plugin-sdk/runtime-fetch |
| Proxy and guarded fetch helpers | openclaw/plugin-sdk/fetch-runtime |
| SSRF dispatcher policy types | openclaw/plugin-sdk/ssrf-dispatcher |
| Approval request/resolution types | openclaw/plugin-sdk/approval-runtime |
| Approval reply payload and command helpers | openclaw/plugin-sdk/approval-reply-runtime |
| Error formatting helpers | openclaw/plugin-sdk/error-runtime |
| Transport readiness waits | openclaw/plugin-sdk/transport-ready-runtime |
| Secure token helpers | openclaw/plugin-sdk/secure-random-runtime |
| Bounded async task concurrency | openclaw/plugin-sdk/concurrency-runtime |
| Required-value assertions for provable invariants | openclaw/plugin-sdk/expect-runtime |
| Numeric coercion | openclaw/plugin-sdk/number-runtime |
| Process-local async lock | openclaw/plugin-sdk/async-lock-runtime |
| File locks | openclaw/plugin-sdk/file-lock |
System event snapshot inspection and consume helpers are only reachable through the deprecated openclaw/plugin-sdk/infra-runtime compatibility layer, with no modern public alternative. A current snapshot holds an opaque id for a single queued occurrence. When handing a snapshot back to consume, carry that value through copies and serialization. Legacy callers without an ID still rely on structural matching, which can turn ambiguous once the queue has churned. Do not assume the ID is persistent or valid across restarts.
File-lock nesting is scoped to the owner. Supply the same reentrantOwner only for nested acquisitions within one logical operation; omit it for regular locking. A process-wide constant must never be used, since unrelated work would then wrongly share the critical section.
Bundled plugins are guarded by a scanner against infra-runtime, so repository code cannot fall back to the broad barrel.
Migrate channel route helpers
Channel route code now relies on openclaw/plugin-sdk/channel-route. The earlier route-key names persist as compatibility aliases:
| Old helper | Modern helper |
|---|---|
channelRouteIdentityKey(...) | channelRouteDedupeKey(...) |
channelRouteKey(...) | channelRouteCompactKey(...) |
The modern route helpers apply { channel, to, accountId, threadId } uniformly across native approvals, reply suppression, inbound dedupe, cron delivery, and session routing.
Avoid introducing new usage of ChannelMessagingAdapter.parseExplicitTarget or resolveChannelRouteTargetWithParser(...) from plugin-sdk/channel-route, both deprecated and kept only for older plugins. New channel plugins should turn to messaging.targetResolver.resolveTarget(...) for target-id normalization and directory-miss fallback, messaging.inferTargetChatType(...) when core needs an early peer kind, and messaging.resolveOutboundSessionRoute(...) for provider-native session and thread identity.
Build and test
pnpm build
pnpm test my-plugin/
Import path reference
The public package export map defines which SDK subpaths are importable. Consult the topical SDK guides linked from SDK overview and pick the narrowest documented public subpath. The compiler inventory in scripts/lib/plugin-sdk-entrypoints.json also holds private-local entries used to build bundled plugins; their inclusion there does not make them public package exports.
This table covers the common migration subset, not the entire SDK surface. The compiler entrypoint inventory lives in scripts/lib/plugin-sdk-entrypoints.json; package exports derive from the public subset.
Reserved bundled-plugin helper seams have been dropped from the public SDK export map, except for documented compatibility facades like the deprecated plugin-sdk/discord shim kept for external plugins that still import the published @openclaw/discord package directly. Owner-specific helpers reside inside the owning plugin package; shared host behavior flows through generic SDK contracts such as plugin-sdk/gateway-runtime, plugin-sdk/security-runtime, and the injected plugin API.
Choose the narrowest import that fits the task. If no export is found, inspect the source at src/plugin-sdk/ or ask maintainers which generic contract should take ownership.
Removed compatibility surfaces
The July 2026 sweep removed the root SDK and compat barrels, the extension API bridge, the expired SDK subpath aliases, unused SDK subpaths, and the public exports for bundled-only SDK modules. Bundled-only modules stay available to their repository owners via private-local build mappings; they cannot be imported from the published package.
Process-global API-provider publication
registerApiProvider(...) and unregisterApiProviders(...) were taken out of openclaw/plugin-sdk/llm. They pushed API transports into process-global state, which lifecycle-owned model runtimes then had to copy into each prepared registry.
Provider plugins should register text-inference providers through api.registerProvider(...). Host-owned code and tests that build an ApiRegistry should register directly on that registry, keeping provider ownership and teardown scoped to the prepared runtime.
Deactivate hook alias
The api.on("deactivate", handler) compatibility alias is gone. Register the same shutdown cleanup with gateway_stop:
// Before
api.on("deactivate", async (event, ctx) => {
await stopPluginService(ctx);
});
// After
api.on("gateway_stop", async (event, ctx) => {
await stopPluginService(ctx);
});
Private testing barrel
openclaw/plugin-sdk/testing was repo-local and never shipped in package artifacts, so it was removed before its 2026-07-28 removeAfter date. Repository tests use focused subpaths such as plugin-sdk/plugin-test-runtime, plugin-sdk/channel-test-helpers, plugin-sdk/channel-target-testing, plugin-sdk/test-env, and plugin-sdk/test-fixtures.
Migration reference
These mappings cover both removed July 2026 surfaces and later-window active deprecations. A mapping is migration guidance, not proof that the old surface remains available; check the compatibility registry and removal timeline for current status.
true
command-status">
Old (openclaw/plugin-sdk/command-auth): buildCommandsMessage,
buildCommandsMessagePaginated, buildHelpMessage.
New (openclaw/plugin-sdk/command-status): same signatures, imported
from the narrower subpath. The command-auth compatibility re-exports
have been removed.
// Before
import { buildHelpMessage } from "openclaw/plugin-sdk/command-auth";
// After
import { buildHelpMessage } from "openclaw/plugin-sdk/command-status";
true
resolveInboundMentionDecision">
Old: resolveMentionGating(params) together with
resolveMentionGatingWithBypass(params), sourced from
openclaw/plugin-sdk/channel-inbound or
openclaw/plugin-sdk/channel-mention-gating.
New: resolveInboundMentionDecision({ facts, policy }) delivers a single decision
object rather than two distinct call shapes.
This is now standard across Discord, iMessage, Matrix, MS Teams, QQBot,
Signal, Telegram, WhatsApp, and Zalo. Slack does not rely on this helper,
since its app_mention event model works differently.
Channel runtime shim and channel actions helpers
openclaw/plugin-sdk/channel-runtime is gone. To register runtime
objects, switch to openclaw/plugin-sdk/channel-runtime-context.
The native message schema helpers that lived in openclaw/plugin-sdk/channel-actions
were dropped along with the raw "actions" channel exports. Capabilities
should be surfaced through the semantic presentation interface instead.
Channel plugins now declare what they render, such as cards, buttons, or
selects, rather than listing the raw action names they accept.
true
createTool() on the plugin">
Old: the tool() factory supplied by openclaw/plugin-sdk/provider-web-search.
New: implement createTool(...) directly on the provider plugin.
OpenClaw no longer depends on the SDK helper to register the tool wrapper.
true
BodyForAgent">
Old: api.runtime.channel.reply.formatInboundEnvelope(...) (plus the
channelEnvelope field carried on inbound message objects) was used to
assemble a flat plaintext prompt envelope from incoming channel messages.
New: BodyForAgent combined with structured user-context blocks.
Channel plugins attach routing metadata, including thread, topic,
reply-to, and reactions, as typed fields instead of folding them into a
prompt string. The formatAgentEnvelope(...) helper remains available for
synthesized assistant-facing envelopes, though inbound plaintext envelopes
are being phased out.
Impacted areas: inbound_claim, message_received, and any custom
channel plugin that performed post-processing on the old envelope text.
true
core thread binding">
Old: api.on("subagent_spawning", handler) that returned
threadBindingReady or deliveryOrigin.
New: let core prepare thread: true subagent bindings via the
channel session-binding adapter. Reach for api.on("subagent_spawned", handler)
only when observing after launch.
// Before
api.on("subagent_spawning", async () => ({
status: "ok",
threadBindingReady: true,
deliveryOrigin: { channel: "discord", to: "channel:123", threadId: "456" },
}));
// After
api.on("subagent_spawned", async (event) => {
await observeSubagentLaunch(event);
});
subagent_spawning, PluginHookSubagentSpawningEvent,
PluginHookSubagentSpawningResult, and
SubagentLifecycleHookRunner.runSubagentSpawning(...) stick around purely as deprecated
compatibility surfaces while external plugins finish migrating, and will be
removed after 2026-08-30.
true
provider catalog types"> Four discovery type aliases now simply wrap the catalog-era types:
| Old alias | New type |
|---|---|
ProviderDiscoveryOrder | ProviderCatalogOrder |
ProviderDiscoveryContext | ProviderCatalogContext |
ProviderDiscoveryResult | ProviderCatalogResult |
ProviderPluginDiscovery | ProviderPluginCatalog |
Both the aliases and the legacy ProviderCapabilities static bag have
been removed. Provider plugins
should rely on explicit provider hooks like buildReplayPolicy,
normalizeToolSchemas, and wrapStreamFn instead of a static object.
true
resolveThinkingProfile">
Old (three separate hooks on ProviderThinkingPolicy):
isBinaryThinking(ctx), supportsXHighThinking(ctx), and
resolveDefaultThinkingLevel(ctx).
New: one resolveThinkingProfile(ctx) returns a
ProviderThinkingProfile containing the canonical id, an optional label, and a
ranked level list. OpenClaw automatically downgrades stale stored values
according to profile rank.
The context carries provider, modelId, optional merged reasoning,
and optional merged model compat facts. Provider plugins can draw on
those catalog facts to surface a model-specific profile only when the
configured request contract permits it.
Implement a single hook instead of three. The legacy hooks are no longer present.
true
contracts.externalAuthProviders"> Old: registering external auth hooks while omitting the provider from the plugin manifest.
New: specify contracts.externalAuthProviders in the plugin manifest
and implement resolveExternalAuthProfiles(...).
{
"contracts": {
"externalAuthProviders": ["anthropic", "openai"]
}
}
true
setup.providers[].envVars">
Old manifest field: providerAuthEnvVars: { anthropic: ["ANTHROPIC_API_KEY"] }.
New: apply the same env-var lookup to setup.providers[].envVars
within the manifest. This centralizes setup/status env metadata in one location
and prevents starting the plugin runtime solely to handle env-var queries.
providerAuthEnvVars is no longer supported.
true
registerMemoryCapability">
Old: three distinct calls - api.registerMemoryPromptSection(...),
api.registerMemoryFlushPlan(...), api.registerMemoryRuntime(...).
New: a single call on the memory-state API -
registerMemoryCapability(pluginId, { promptBuilder, flushPlanResolver, runtime }).
Identical slots, one registration call. Additive prompt and corpus helpers
(registerMemoryPromptSupplement, registerMemoryCorpusSupplement) remain
unchanged.
Memory embedding provider API
Old: api.registerMemoryEmbeddingProvider(...) together with
contracts.memoryEmbeddingProviders.
New: api.registerEmbeddingProvider(...) together with
contracts.embeddingProviders.
The generic embedding provider contract works beyond memory and serves as the recommended route for new providers. The memory-specific registration API stays active as deprecated compatibility while current providers transition. Plugin inspection flags non-bundled usage as compatibility debt.
true
OutboundDeliveryResult">
Old: return { ok, messageId, error } via
ChannelSendRawResult and normalize it with
createRawChannelSendResultAdapter(...).
New: return OutboundDeliveryResult fields and associate the channel using
createAttachedChannelResultAdapter(...). Failed sends should raise an exception rather
than returning an error string. The raw result type stays accessible until
the subsequent plugin-SDK major release.
Subagent session messages types renamed
Two legacy type aliases remain exported from src/plugins/runtime/types.ts:
| Old | New |
|---|---|
SubagentReadSessionParams | SubagentGetSessionMessagesParams |
SubagentReadSessionResult | SubagentGetSessionMessagesResult |
The runtime method readSession is deprecated in favor of
getSessionMessages. Same signature; the old method delegates to the
new one.
Removed session and transcript file APIs
The SQLite session/transcript flip removes or deprecates plugin-facing APIs
that exposed active sessions.json stores, JSONL transcript paths, or lists
of session files. Runtime plugins should rely on session identity and SDK runtime
helpers instead of resolving or modifying active files.
| Migrating surface | Replacement |
|---|---|
Deprecated loadSessionStore(...), updateSessionStore(...), and resolveSessionStoreEntry(...), including package-root loadSessionStore(...) | getSessionEntry(...), listSessionEntries(...), and row-level session mutations. |
Deprecated resolveSessionFilePath(...) | Session identity (sessionKey, sessionId, and SDK runtime target helpers) plus Gateway methods that act on the current session. |
Deprecated package-root saveSessionStore(...) and removed SDK file-store writes | Gateway-owned session runtime APIs; plugin code should request or alter session state through documented runtime/context helpers instead of writing the active store file. |
Removed resolveSessionTranscriptPathInDir(...) and resolveAndPersistSessionFile(...) | Session identity and Gateway methods that act on the current session. |
readLatestAssistantTextFromSessionTranscript(...) | Identity-backed transcript readers provided by the current runtime context, or Gateway history/session methods when the plugin is outside the transcript owner path. |
SessionTranscriptUpdate.sessionFile | SessionTranscriptUpdate.target with agentId, sessionKey, and sessionId. |
Memory sync inputs such as sessionFiles | Identity-backed transcript/session sources supplied by the host; do not scan active JSONL files for live sessions. |
Runtime options named transcriptPath or sessionFile for active sessions | sessionTarget/runtime target objects that carry storage-neutral session identity. |
Legacy JSONL transcript files remain valid as import, archive, export, and support artifacts. They are no longer the steady-state runtime contract for active sessions.
Official plugins released with v2026.7.1-beta.5 imported the four
deprecated helpers above. openclaw/plugin-sdk/session-store-runtime preserves
that exact bridge through 2026-10-12; new plugins must adopt the replacements.
resolveStorePath(...) remains a supported SDK helper and is excluded from
this deprecation.
openclaw plugins inspect --all --runtime flags plugins that are not bundled but whose load failures or diagnostics still point to the file APIs that were removed. The advisory scan in @openclaw/plugin-inspector requires version 0.3.17 or later, ensuring external package checks also catch whole-store session helpers, session file-path helpers, legacy transcript file targets, and low-level transcript helpers prior to shipping.
true
V2 host-capability contract">
For new or updated harness plugins, implement AgentHarnessV2 and rely on AgentHarnessAttemptParamsV2, EmbeddedRunAttemptParamsV2, or AgentHarnessSideQuestionParamsV2. The V2 parameter types demand hostCapabilities, which aligns with what core provides at the selected-harness boundary. Any plugin adopting these V2 contracts must specify openclaw.compat.pluginApi: ">=2026.8.1" (or a newer minimum) in its package manifest, so an older host refuses to load the plugin beforehand.
Through 2026-10-12, existing plugins can still implement AgentHarness and build the legacy AgentHarnessAttemptParams, EmbeddedRunAttemptParams, or AgentHarnessSideQuestionParams types without that field. These contracts keep the capability optional only for source compatibility; they do not introduce a runtime path without capabilities. To migrate, alter the imported type name and attach tool or native-action surfaces via params.hostCapabilities.
true
runtime.tasks.managedFlows">
Old: runtime.tasks.flow (singular) returned a live accessor for task flows.
New: runtime.tasks.managedFlows preserves the managed TaskFlow mutation runtime for plugins that create, update, cancel, or run child tasks from a flow. When the plugin only needs DTO-based reads, use runtime.tasks.flows.
// Before
const flow = api.runtime.tasks.flow.fromToolContext(ctx);
// After
const flow = api.runtime.tasks.managedFlows.fromToolContext(ctx);
The legacy aliases disappeared in July 2026.
true
agent tool-result middleware">
Discussed in How to migrate earlier. Listed here for thoroughness: the removed embedded-runner-only api.registerEmbeddedExtensionFactory(...) path gets replaced by api.registerAgentToolResultMiddleware(...) with an explicit runtime list in contracts.agentToolResultMiddleware.
true
OpenClawConfig">
The OpenClawSchemaType root-SDK alias is gone. Adopt the canonical OpenClawConfig name instead.
// Before
import type { OpenClawSchemaType } from "openclaw/plugin-sdk";
// After
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
Note
Deprecations at the extension level (within bundled channel/provider plugins under
extensions/) are tracked in their ownapi.tsandruntime-api.tsbarrels. These do not affect third-party plugin contracts and are omitted here. If you directly consume a bundled plugin's local barrel, check the deprecation comments in that barrel before upgrading.
Talk and realtime voice migration
Realtime voice, telephony, meeting, and browser Talk code all share a single Talk session controller exported by openclaw/plugin-sdk/realtime-voice. That controller owns the common Talk event envelope, active turn state, capture state, output-audio state, recent event history, and stale-turn rejection. Vendor-specific realtime sessions are owned by provider plugins. Browser-meeting plugins rely on openclaw/plugin-sdk/meeting-runtime for session, browser, audio, node-host, agent-consult, and voice-call mechanics, then implement MeetingPlatformAdapter for URL rules, DOM scripts, manual-action mapping, captions, creation, and dial-in plans. Platform REST APIs, OAuth, artifacts, selectors, and wire names stay within the plugin. Browser permission plans receive the requested meeting URL, so each platform can grant only its exact supported origins. Session runtimes must also normalize platform-specific live health after confirmed browser departure; historical transcript fields may remain, but caption and audio readiness must not stay active after leave.
All bundled surfaces operate on the shared controller: browser relay, managed-room handoff, voice-call realtime, voice-call streaming STT, Google Meet realtime, and native push-to-talk. Gateway advertises one live Talk event channel in hello-ok.features.events: talk.event.
New code should avoid calling createTalkEventSequencer(...) directly unless it is implementing a low-level adapter or test fixture. Use the shared controller so turn-scoped events cannot be emitted without a turn id, stale turnEnd / turnCancel calls cannot overwrite a newer active turn, and output-audio lifecycle events remain consistent across telephony, meetings, browser relay, managed-room handoff, and native Talk clients.
The public API shape:
// Gateway-owned Talk session API.
await gateway.request("talk.session.create", {
mode: "realtime",
transport: "gateway-relay",
brain: "agent-consult",
sessionKey: "main",
});
await gateway.request("talk.session.appendAudio", { sessionId, audioBase64 });
await gateway.request("talk.session.cancelOutput", { sessionId, reason: "barge-in" });
await gateway.request("talk.session.submitToolResult", {
sessionId,
callId,
result: { status: "working" },
options: { willContinue: true },
});
await gateway.request("talk.session.submitToolResult", {
sessionId,
callId,
result: { status: "already_delivered" },
options: { suppressResponse: true },
});
await gateway.request("talk.session.submitToolResult", { sessionId, callId, result });
await gateway.request("talk.session.close", { sessionId });
// Client-owned provider session API.
await gateway.request("talk.client.create", {
mode: "realtime",
transport: "webrtc",
brain: "agent-consult",
sessionKey: "main",
});
await gateway.request("talk.client.toolCall", { sessionKey, callId, name, args });
await gateway.request("talk.client.steer", { sessionKey, text, mode: "steer" });
Browser-owned WebRTC/provider-websocket sessions use talk.client.create, since the browser handles provider negotiation and media transport while the Gateway manages credentials, instructions, and tool policy. talk.session.* serves as the common Gateway-managed surface for gateway-relay realtime, gateway-relay transcription, and managed-room native STT/TTS sessions.
Legacy configs that position realtime selectors next to talk.provider / talk.providers should be fixed with openclaw doctor --fix; runtime Talk does not reinterpret speech/TTS provider config as realtime provider config.
The supported talk.session.create combinations are deliberately limited:
| Mode | Transport | Brain | Owner | Notes |
|---|---|---|---|---|
realtime | gateway-relay | agent-consult | Gateway | Full-duplex provider audio bridged through the Gateway; tool calls route through the agent-consult tool. |
transcription | gateway-relay | none | Gateway | Streaming STT only; callers send input audio and receive transcript events. |
stt-tts | managed-room | agent-consult | Native/client room | Push-to-talk and walkie-talkie style rooms where the client owns capture/playback and the Gateway owns turn state. |
stt-tts | managed-room | direct-tools | Native/client room | Admin-only room mode for trusted first-party surfaces that execute Gateway tool actions directly. |
For readers coming from the older talk.realtime.* / talk.transcription.* / talk.handoff.* families, all of which have been removed, the following method mapping applies:
| Old | New |
|---|---|
talk.realtime.session | talk.client.create |
talk.realtime.toolCall | talk.client.toolCall |
talk.realtime.relayAudio | talk.session.appendAudio |
talk.realtime.relayCancel | talk.session.cancelOutput |
talk.realtime.relayToolResult | talk.session.submitToolResult |
talk.realtime.relayStop | talk.session.close |
talk.transcription.session | talk.session.create({ mode: "transcription" }) |
talk.transcription.relayAudio | talk.session.appendAudio |
talk.transcription.relayCancel | talk.session.close |
talk.transcription.relayStop | talk.session.close |
talk.handoff.create | talk.session.create({ transport: "managed-room" }) |
talk.handoff.revoke | talk.session.close |
The shared control vocabulary stays intentionally constrained as well:
| Method | Applies to | Contract |
|---|---|---|
talk.session.appendAudio | realtime/gateway-relay, transcription/gateway-relay | Take a base64 PCM audio chunk and append it to the provider session tied to the same Gateway connection. |
talk.session.cancelOutput | realtime/gateway-relay | Halt assistant audio output without necessarily concluding the user turn. |
talk.session.submitToolResult | realtime/gateway-relay | Finish a provider tool call once its bridge exposes any asynchronous completion; supply options.willContinue for interim results or, where supported, options.suppressResponse to suppress another assistant response. |
talk.session.steer | agent-backed Talk sessions | Dispatch spoken status, steer, cancel, or followup commands to the active embedded run resolved from the Talk session. |
talk.session.close | all unified sessions | Terminate relay sessions or clear managed-room state, then discard the unified session id. |
Avoid introducing provider or platform exceptions in core to achieve this. Talk session semantics belong to core. Vendor session setup belongs to provider plugins. Telephony and meeting adapters belong to voice-call and Google Meet. Device capture and playback UX belongs to browser and native apps.
Removal timeline
| When | What happens |
|---|---|
| Now | Deprecated surfaces that can warn now emit runtime warnings; repository guards block deprecated SDK imports from core and bundled plugins. |
| Pending owner decision | Entries lacking removeAfter or removalGate stay deprecated and ineligible until their owner publishes a gate. |
Each compat record's removeAfter date | That dated surface becomes removable; pnpm plugins:boundary-report --fail-on-eligible-compat fails CI on or after that date. |
| Next Plugin SDK major | inbound-reply-dispatch reaches its explicit next-plugin-sdk-major gate; it cannot be removed by date before that version boundary. |
The public SDK subpaths that remain below have removal windows backed by the registry. The July 30 rows were dropped after the early maintainer-authorized sweep: unused subpaths were removed, earlier compatibility aliases were removed, and bundled-only modules were downgraded to private-local build mappings.
The compatibility subpaths agent-config-primitives,
channel-logging, channel-secret-runtime, channel-streaming,
group-access, matrix, text-runtime, and zod were pulled from service ahead of schedule in August 2026, following explicit approval from the SDK owners. For the targeted alternatives, check the Plugin SDK subpath catalog and bring in zod straight from the zod package. inbound-reply-dispatch stays active up to the upcoming Plugin SDK major.
| Removal gate | Tier | SDK subpaths |
|---|---|---|
2026-09-01 | Earlier compatibility deprecations | channel-lifecycle, channel-message, channel-reply-pipeline, config-runtime, infra-runtime |
next-plugin-sdk-major | Major-version compatibility gate | inbound-reply-dispatch |
2026-10-01 | Media legacy projection | agent-media-payload, plus the non-subpath MsgContext Media* fields, channel inbound media payload builders, buildMediaPayload, hook media aliases, and {{Media*}} templates |
Every core plugin has completed its migration. External plugins are advised to move over before the next major version lands. Execute pnpm plugins:boundary-report to identify which compat records are approaching their deadline for the surfaces your plugin relies on.
Related
- Getting Started - build your first plugin
- SDK Overview - full subpath import reference
- Channel Plugins - building channel plugins
- Provider Plugins - building provider plugins
- Plugin Internals - architecture deep dive
- Plugin Manifest - manifest schema reference