docs/plugins/sdk-migration.md
OpenClaw has moved from a broad backwards-compatibility layer to a modern plugin architecture with focused, documented imports. If your plugin was built before the new architecture, this guide helps you migrate.
The old plugin system provided two wide-open surfaces that let plugins import anything they needed from a single entry point:
openclaw/plugin-sdk/compat — a single import that re-exported dozens of
helpers. It was introduced to keep older hook-based plugins working while the
new plugin architecture was being built.openclaw/plugin-sdk/infra-runtime — a broad runtime helper barrel that
mixed system events, heartbeat state, delivery queues, fetch/proxy helpers,
file helpers, approval types, and unrelated utilities.openclaw/plugin-sdk/config-runtime — a broad config compatibility barrel
that still carries deprecated direct load/write helpers during the migration
window.openclaw/extension-api — a bridge that gave plugins direct access to
host-side helpers like the embedded agent runner.api.registerEmbeddedExtensionFactory(...) — a removed Pi-only bundled
extension hook that could observe embedded-runner events such as
tool_result.The broad import surfaces are now deprecated. They still work at runtime, but new plugins must not use them, and existing plugins should migrate before the next major release removes them. The Pi-only embedded extension factory registration API has been removed; use tool-result middleware instead.
OpenClaw does not remove or reinterpret documented plugin behavior in the same change that introduces a replacement. Breaking contract changes must first go through a compatibility adapter, diagnostics, docs, and a deprecation window. That applies to SDK imports, manifest fields, setup APIs, hooks, and runtime registration behavior.
<Warning> The backwards-compatibility layer will be removed in a future major release. Plugins that still import from these surfaces will break when that happens. Pi-only embedded extension factory registrations already no longer load. </Warning>The old approach caused problems:
The modern plugin SDK fixes this: each import path (openclaw/plugin-sdk/\<subpath\>)
is a small, self-contained module with a clear purpose and documented contract.
Legacy provider convenience seams for bundled channels are also gone.
Channel-branded helper seams were private mono-repo shortcuts, 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.
Current bundled provider examples:
api.ts /
contract-api.ts seamapi.tsapi.tsFor external plugins, compatibility work follows this order:
Maintainers can audit the current migration queue with
pnpm plugins:boundary-report. Use pnpm plugins:boundary-report:summary for
compact counts, --owner <id> for one plugin or compatibility owner, and
pnpm plugins:boundary-report:ci when a CI gate should fail on due
compatibility records, cross-owner reserved SDK imports, or unused reserved SDK
subpaths. The report groups deprecated
compatibility records by removal date, counts local code/docs references,
surfaces cross-owner reserved SDK imports, and summarizes the private
memory-host SDK bridge so compatibility cleanup stays explicit instead of
relying on ad hoc searches. Reserved SDK subpaths must have tracked owner usage;
unused reserved helper exports should be removed from the public SDK.
If a manifest field is still accepted, plugin authors can keep using it until the docs and diagnostics say otherwise. New code should prefer the documented replacement, but existing plugins should not break during ordinary minor releases.
Config writes must go through the transactional helpers and choose an
after-write policy:
```typescript
await api.runtime.config.mutateConfigFile({
afterWrite: { mode: "auto" },
mutate(draft) {
draft.plugins ??= {};
},
});
```
Use `afterWrite: { mode: "restart", reason: "..." }` when the caller knows
the change requires a clean gateway restart, and
`afterWrite: { mode: "none", reason: "..." }` only when the caller owns the
follow-up and deliberately wants to suppress the reload planner.
Mutation results include a typed `followUp` summary for tests and logging;
the gateway remains responsible for applying or scheduling the restart.
`loadConfig` and `writeConfigFile` remain as deprecated compatibility
helpers for external plugins during the migration window and warn once with
the `runtime-config-load-write` compatibility code. Bundled plugins and repo
runtime code are protected by scanner guardrails in
`pnpm check:deprecated-internal-config-api` 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 have
zero allowed ambient `loadConfig()` calls.
New plugin code should also avoid importing the broad
`openclaw/plugin-sdk/config-runtime` compatibility barrel. Use the narrow
SDK subpath that matches the job:
| Need | Import |
| --- | --- |
| Config types such as `OpenClawConfig` | `openclaw/plugin-sdk/config-types` |
| Already-loaded config assertions and plugin-entry config lookup | `openclaw/plugin-sdk/plugin-config-runtime` |
| 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` |
Bundled plugins and their tests are scanner-guarded against the broad
barrel so imports and mocks stay local to the behavior they need. The broad
barrel still exists for external compatibility, but new code should not
depend on it.
```typescript
// Pi and Codex runtime dynamic tools
api.registerAgentToolResultMiddleware(async (event) => {
return compactToolResult(event);
}, {
runtimes: ["pi", "codex"],
});
```
Update the plugin manifest at the same time:
```json
{
"contracts": {
"agentToolResultMiddleware": ["pi", "codex"]
}
}
```
External plugins cannot register tool-result middleware because it can
rewrite high-trust tool output before the model sees it.
Key changes:
- Replace `approvalCapability.handler.loadRuntime(...)` with
`approvalCapability.nativeRuntime`
- Move approval-specific auth/delivery off legacy `plugin.auth` /
`plugin.approvals` wiring and onto `approvalCapability`
- `ChannelPlugin.approvals` has been removed from the public channel-plugin
contract; move delivery/native/render fields onto `approvalCapability`
- `plugin.auth` remains for channel login/logout flows only; approval auth
hooks there are no longer read by core
- Register channel-owned runtime objects such as clients, tokens, or Bolt
apps through `openclaw/plugin-sdk/channel-runtime-context`
- Do not send plugin-owned reroute notices from native approval handlers;
core now owns routed-elsewhere notices from actual delivery results
- When passing `channelRuntime` into `createChannelManager(...)`, provide a
real `createPluginRuntime().channel` surface. Partial stubs are rejected.
See `/plugins/sdk-channel-plugins` for the current approval capability
layout.
```typescript
// 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,
});
```
If your caller does not intentionally rely on shell fallback, do not set
`allowShellFallback` and handle the thrown error instead.
```bash
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/
```
```typescript
// 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, use the injected plugin runtime instead of importing
directly:
```typescript
// Before (deprecated extension-api bridge)
import { runEmbeddedPiAgent } from "openclaw/extension-api";
const result = await runEmbeddedPiAgent({ sessionId, prompt });
// After (injected runtime)
const result = await api.runtime.agent.runEmbeddedPiAgent({ sessionId, prompt });
```
The same pattern 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.*` |
| Need | Import |
| --- | --- |
| System event queue helpers | `openclaw/plugin-sdk/system-event-runtime` |
| Heartbeat 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 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` |
| Numeric coercion | `openclaw/plugin-sdk/number-runtime` |
| Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` |
| File locks | `openclaw/plugin-sdk/file-lock` |
Bundled plugins are scanner-guarded against `infra-runtime`, so repo code
cannot regress to the broad barrel.
| Old helper | Modern helper |
| --- | --- |
| `channelRouteIdentityKey(...)` | `channelRouteDedupeKey(...)` |
| `channelRouteKey(...)` | `channelRouteCompactKey(...)` |
| `ComparableChannelTarget` | `ChannelRouteParsedTarget` |
| `resolveComparableTargetForChannel(...)` | `resolveRouteTargetForChannel(...)` |
| `resolveComparableTargetForLoadedChannel(...)` | `resolveRouteTargetForLoadedChannel(...)` |
| `comparableChannelTargetsMatch(...)` | `channelRouteTargetsMatchExact(...)` |
| `comparableChannelTargetsShareRoute(...)` | `channelRouteTargetsShareConversation(...)` |
The modern route helpers normalize `{ channel, to, accountId, threadId }`
consistently across native approvals, reply suppression, inbound dedupe,
cron delivery, and session routing. If your plugin owns custom target
grammar, use `resolveChannelRouteTargetWithParser(...)` to adapt that
parser into the same route target contract.
This table is intentionally the common migration subset, not the full SDK
surface. The full list of 200+ entrypoints lives in
scripts/lib/plugin-sdk-entrypoints.json.
Reserved bundled-plugin helper seams have been retired from the public SDK
export map except for explicitly documented compatibility facades such as the
deprecated plugin-sdk/discord shim retained for the published
@openclaw/[email protected] package. Owner-specific helpers live inside the
owning plugin package; shared host behavior should move through generic SDK
contracts such as plugin-sdk/gateway-runtime, plugin-sdk/security-runtime,
and plugin-sdk/plugin-config-runtime.
Use the narrowest import that matches the job. If you cannot find an export,
check the source at src/plugin-sdk/ or ask maintainers which generic contract
should own it.
Narrower deprecations that apply across the plugin SDK, provider contract, runtime surface, and manifest. Each one still works today but will be removed in a future major release. The entry below every item maps the old API to its canonical replacement.
<AccordionGroup> <Accordion title="command-auth help builders → command-status"> **Old (`openclaw/plugin-sdk/command-auth`)**: `buildCommandsMessage`, `buildCommandsMessagePaginated`, `buildHelpMessage`.**New (`openclaw/plugin-sdk/command-status`)**: same signatures, same
exports — just imported from the narrower subpath. `command-auth`
re-exports them as compat stubs.
```typescript
// Before
import { buildHelpMessage } from "openclaw/plugin-sdk/command-auth";
// After
import { buildHelpMessage } from "openclaw/plugin-sdk/command-status";
```
**New**: `resolveInboundMentionDecision({ facts, policy })` — returns a
single decision object instead of two split calls.
Downstream channel plugins (Slack, Discord, Matrix, MS Teams) have already
switched.
`channelActions*` helpers in `openclaw/plugin-sdk/channel-actions` are
deprecated alongside raw "actions" channel exports. Expose capabilities
through the semantic `presentation` surface instead — channel plugins
declare what they render (cards, buttons, selects) rather than which raw
action names they accept.
**New**: implement `createTool(...)` directly on the provider plugin.
OpenClaw no longer needs the SDK helper to register the tool wrapper.
**New**: `BodyForAgent` plus structured user-context blocks. Channel
plugins attach routing metadata (thread, topic, reply-to, reactions) as
typed fields instead of concatenating them into a prompt string. The
`formatAgentEnvelope(...)` helper is still supported for synthesized
assistant-facing envelopes, but inbound plaintext envelopes are on the
way out.
Affected areas: `inbound_claim`, `message_received`, and any custom
channel plugin that post-processed `channelEnvelope` text.
| Old alias | New type |
| ------------------------- | ------------------------- |
| `ProviderDiscoveryOrder` | `ProviderCatalogOrder` |
| `ProviderDiscoveryContext`| `ProviderCatalogContext` |
| `ProviderDiscoveryResult` | `ProviderCatalogResult` |
| `ProviderPluginDiscovery` | `ProviderPluginCatalog` |
Plus the legacy `ProviderCapabilities` static bag — provider plugins
should use explicit provider hooks such as `buildReplayPolicy`,
`normalizeToolSchemas`, and `wrapStreamFn` rather than a static object.
**New**: a single `resolveThinkingProfile(ctx)` that returns a
`ProviderThinkingProfile` with the canonical `id`, optional `label`, and
ranked level list. OpenClaw downgrades stale stored values by profile
rank automatically.
Implement one hook instead of three. The legacy hooks keep working during
the deprecation window but are not composed with the profile result.
**New**: declare `contracts.externalAuthProviders` in the plugin manifest
**and** implement `resolveExternalAuthProfiles(...)`. The old "auth
fallback" path emits a warning at runtime and will be removed.
```json
{
"contracts": {
"externalAuthProviders": ["anthropic", "openai"]
}
}
```
**New**: mirror the same env-var lookup into `setup.providers[].envVars`
on the manifest. This consolidates setup/status env metadata in one
place and avoids booting the plugin runtime just to answer env-var
lookups.
`providerAuthEnvVars` remains supported through a compatibility adapter
until the deprecation window closes.
**New**: one call on the memory-state API —
`registerMemoryCapability(pluginId, { promptBuilder, flushPlanResolver, runtime })`.
Same slots, single registration call. Additive memory helpers
(`registerMemoryPromptSupplement`, `registerMemoryCorpusSupplement`,
`registerMemoryEmbeddingProvider`) are not affected.
| Old | New |
| ----------------------------- | ------------------------------- |
| `SubagentReadSessionParams` | `SubagentGetSessionMessagesParams` |
| `SubagentReadSessionResult` | `SubagentGetSessionMessagesResult` |
The runtime method `readSession` is deprecated in favor of
`getSessionMessages`. Same signature; the old method calls through to the
new one.
**New**: `runtime.tasks.managedFlows` keeps the managed TaskFlow mutation
runtime for plugins that create, update, cancel, or run child tasks from a
flow. Use `runtime.tasks.flows` when the plugin only needs DTO-based reads.
```typescript
// Before
const flow = api.runtime.tasks.flow.fromToolContext(ctx);
// After
const flow = api.runtime.tasks.managedFlows.fromToolContext(ctx);
```
```typescript
// Before
import type { OpenClawSchemaType } from "openclaw/plugin-sdk";
// After
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-schema";
```
| When | What happens |
|---|---|
| Now | Deprecated surfaces emit runtime warnings |
| Next major release | Deprecated surfaces will be removed; plugins still using them will fail |
All core plugins have already been migrated. External plugins should migrate before the next major release.
Set these environment variables while you work on migrating:
OPENCLAW_SUPPRESS_PLUGIN_SDK_COMPAT_WARNING=1 openclaw gateway run
OPENCLAW_SUPPRESS_EXTENSION_API_WARNING=1 openclaw gateway run
This is a temporary escape hatch, not a permanent solution.