Load pipeline
At startup, OpenClaw does roughly this:- discover candidate plugin roots
- read native or compatible bundle manifests and package metadata
- reject unsafe candidates
- normalize plugin config (
plugins.enabled,allow,deny,entries,slots,load.paths) - decide enablement for each candidate
- load enabled native modules: built bundled modules use a native loader; unbuilt native plugins use jiti
- call native
register(api)hooks and collect registrations into the plugin registry - expose the registry to commands/runtime surfaces
activate is a legacy alias for register — the loader resolves whichever is present (def.register ?? def.activate) and calls it at the same point. All bundled plugins use register; prefer register for new plugins.Manifest-first behavior
The manifest is the control-plane source of truth. OpenClaw uses it to:- identify the plugin
- discover declared channels/skills/config schema or bundle capabilities
- validate
plugins.entries.<id>.config - augment Control UI labels/placeholders
- show install/catalog metadata
- preserve cheap activation and setup descriptors without loading plugin runtime
activation and setup blocks stay on the control plane.
They are metadata-only descriptors for activation planning and setup discovery;
they do not replace runtime registration, register(...), or setupEntry.
The first live activation consumers now use manifest command, channel, and provider hints
to narrow plugin loading before broader registry materialization:
- CLI loading narrows to plugins that own the requested primary command
- channel setup/plugin resolution narrows to plugins that own the requested channel id
- explicit provider setup/runtime resolution narrows to plugins that own the requested provider id
activation.* planner hints from manifest ownership
fallback such as providers, channels, commandAliases, setup.providers,
contracts.tools, and hooks. That reason split is the compatibility boundary:
existing plugin metadata keeps working, while new code can detect broad hints
or fallback behavior without changing runtime loading semantics.
Setup discovery now prefers descriptor-owned ids such as setup.providers and
setup.cliBackends to narrow candidate plugins before it falls back to
setup-api for plugins that still need setup-time runtime hooks. If more than
one discovered plugin claims the same normalized setup provider or CLI backend
id, setup lookup refuses the ambiguous owner instead of relying on discovery
order.
What the loader caches
OpenClaw keeps short in-process caches for:- discovery results
- manifest registry data
- loaded plugin registries
- Set
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE=1orOPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE=1to disable these caches. - Tune cache windows with
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MSandOPENCLAW_PLUGIN_MANIFEST_CACHE_MS.
Registry model
Loaded plugins do not directly mutate random core globals. They register into a central plugin registry. The registry tracks:- plugin records (identity, source, origin, status, diagnostics)
- tools
- legacy hooks and typed hooks
- channels
- providers
- gateway RPC handlers
- HTTP routes
- CLI registrars
- background services
- plugin-owned commands
- plugin module -> registry registration
- core runtime -> registry consumption
Conversation binding callbacks
Plugins that bind a conversation can react when an approval is resolved. Useapi.onConversationBindingResolved(...) to receive a callback after a bind
request is approved or denied:
status:"approved"or"denied"decision:"allow-once","allow-always", or"deny"binding: the resolved binding for approved requestsrequest: the original request summary, detach hint, sender id, and conversation metadata
Provider runtime hooks
Provider plugins have three layers:- Manifest metadata for cheap pre-runtime lookup:
providerAuthEnvVars,providerAuthAliases,providerAuthChoices, andchannelEnvVars. - Config-time hooks:
catalog(legacydiscovery) plusapplyConfigDefaults. - Runtime hooks: 40+ optional hooks covering auth, model resolution, stream wrapping, thinking levels, replay policy, and usage endpoints. See the full list under Hook order and usage.
providerAuthEnvVars when the provider has env-based credentials
that generic auth/status/model-picker paths should see without loading plugin
runtime. Use manifest providerAuthAliases when one provider id should reuse
another provider id’s env vars, auth profiles, config-backed auth, and API-key
onboarding choice. Use manifest providerAuthChoices when onboarding/auth-choice
CLI surfaces should know the provider’s choice id, group labels, and simple
one-flag auth wiring without loading provider runtime. Keep provider runtime
envVars for operator-facing hints such as onboarding labels or OAuth
client-id/client-secret setup vars.
Use manifest channelEnvVars when a channel has env-driven auth or setup that
generic shell-env fallback, config/status checks, or setup prompts should see
without loading channel runtime.
Hook order and usage
For model/provider plugins, OpenClaw calls hooks in this rough order. The “When to use” column is the quick decision guide.| # | Hook | What it does | When to use |
|---|---|---|---|
| 1 | catalog | Publish provider config into models.providers during models.json generation | Provider owns a catalog or base URL defaults |
| 2 | applyConfigDefaults | Apply provider-owned global config defaults during config materialization | Defaults depend on auth mode, env, or provider model-family semantics |
| — | (built-in model lookup) | OpenClaw tries the normal registry/catalog path first | (not a plugin hook) |
| 3 | normalizeModelId | Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |
| 4 | normalizeTransport | Normalize provider-family api / baseUrl before generic model assembly | Provider owns transport cleanup for custom provider ids in the same transport family |
| 5 | normalizeConfig | Normalize models.providers.<id> before runtime/provider resolution | Provider needs config cleanup that should live with the plugin; bundled Google-family helpers also backstop supported Google config entries |
| 6 | applyNativeStreamingUsageCompat | Apply native streaming-usage compat rewrites to config providers | Provider needs endpoint-driven native streaming usage metadata fixes |
| 7 | resolveConfigApiKey | Resolve env-marker auth for config providers before runtime auth loading | Provider has provider-owned env-marker API-key resolution; amazon-bedrock also has a built-in AWS env-marker resolver here |
| 8 | resolveSyntheticAuth | Surface local/self-hosted or config-backed auth without persisting plaintext | Provider can operate with a synthetic/local credential marker |
| 9 | resolveExternalAuthProfiles | Overlay provider-owned external auth profiles; default persistence is runtime-only for CLI/app-owned creds | Provider reuses external auth credentials without persisting copied refresh tokens; declare contracts.externalAuthProviders in the manifest |
| 10 | shouldDeferSyntheticProfileAuth | Lower stored synthetic profile placeholders behind env/config-backed auth | Provider stores synthetic placeholder profiles that should not win precedence |
| 11 | resolveDynamicModel | Sync fallback for provider-owned model ids not in the local registry yet | Provider accepts arbitrary upstream model ids |
| 12 | prepareDynamicModel | Async warm-up, then resolveDynamicModel runs again | Provider needs network metadata before resolving unknown ids |
| 13 | normalizeResolvedModel | Final rewrite before the embedded runner uses the resolved model | Provider needs transport rewrites but still uses a core transport |
| 14 | contributeResolvedModelCompat | Contribute compat flags for vendor models behind another compatible transport | Provider recognizes its own models on proxy transports without taking over the provider |
| 15 | capabilities | Provider-owned transcript/tooling metadata used by shared core logic | Provider needs transcript/provider-family quirks |
| 16 | normalizeToolSchemas | Normalize tool schemas before the embedded runner sees them | Provider needs transport-family schema cleanup |
| 17 | inspectToolSchemas | Surface provider-owned schema diagnostics after normalization | Provider wants keyword warnings without teaching core provider-specific rules |
| 18 | resolveReasoningOutputMode | Select native vs tagged reasoning-output contract | Provider needs tagged reasoning/final output instead of native fields |
| 19 | prepareExtraParams | Request-param normalization before generic stream option wrappers | Provider needs default request params or per-provider param cleanup |
| 20 | createStreamFn | Fully replace the normal stream path with a custom transport | Provider needs a custom wire protocol, not just a wrapper |
| 21 | wrapStreamFn | Stream wrapper after generic wrappers are applied | Provider needs request headers/body/model compat wrappers without a custom transport |
| 22 | resolveTransportTurnState | Attach native per-turn transport headers or metadata | Provider wants generic transports to send provider-native turn identity |
| 23 | resolveWebSocketSessionPolicy | Attach native WebSocket headers or session cool-down policy | Provider wants generic WS transports to tune session headers or fallback policy |
| 24 | formatApiKey | Auth-profile formatter: stored profile becomes the runtime apiKey string | Provider stores extra auth metadata and needs a custom runtime token shape |
| 25 | refreshOAuth | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared pi-ai refreshers |
| 26 | buildAuthDoctorHint | Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure |
| 27 | matchesContextOverflowError | Provider-owned context-window overflow matcher | Provider has raw overflow errors generic heuristics would miss |
| 28 | classifyFailoverReason | Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc |
| 29 | isCacheTtlEligible | Prompt-cache policy for proxy/backhaul providers | Provider needs proxy-specific cache TTL gating |
| 30 | buildMissingAuthMessage | Replacement for the generic missing-auth recovery message | Provider needs a provider-specific missing-auth recovery hint |
| 31 | suppressBuiltInModel | Stale upstream model suppression plus optional user-facing error hint | Provider needs to hide stale upstream rows or replace them with a vendor hint |
| 32 | augmentModelCatalog | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in models list and pickers |
| 33 | resolveThinkingProfile | Model-specific /think level set, display labels, and default | Provider exposes a custom thinking ladder or binary label for selected models |
| 34 | isBinaryThinking | On/off reasoning toggle compatibility hook | Provider exposes only binary thinking on/off |
| 35 | supportsXHighThinking | xhigh reasoning support compatibility hook | Provider wants xhigh on only a subset of models |
| 36 | resolveDefaultThinkingLevel | Default /think level compatibility hook | Provider owns default /think policy for a model family |
| 37 | isModernModelRef | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching |
| 38 | prepareRuntimeAuth | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential |
| 39 | resolveUsageAuth | Resolve usage/billing credentials for /usage and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential |
| 40 | fetchUsageSnapshot | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser |
| 41 | createEmbeddingProvider | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin |
| 42 | buildReplayPolicy | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) |
| 43 | sanitizeReplayHistory | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers |
| 44 | validateReplayTurns | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation |
| 45 | onModelSelected | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active |
normalizeModelId, normalizeTransport, and normalizeConfig first check the
matched provider plugin, then fall through other hook-capable provider plugins
until one actually changes the model id or transport/config. That keeps
alias/compat provider shims working without requiring the caller to know which
bundled plugin owns the rewrite. If no provider hook rewrites a supported
Google-family config entry, the bundled Google config normalizer still applies
that compatibility cleanup.
If the provider needs a fully custom wire protocol or custom request executor,
that is a different class of extension. These hooks are for provider behavior
that still runs on OpenClaw’s normal inference loop.
Provider example
Built-in examples
Bundled provider plugins combine the hooks above to fit each vendor’s catalog, auth, thinking, replay, and usage needs. The authoritative hook set lives with each plugin underextensions/; this page illustrates the shapes rather than
mirroring the list.
Pass-through catalog providers
Pass-through catalog providers
OpenRouter, Kilocode, Z.AI, xAI register
catalog plus
resolveDynamicModel / prepareDynamicModel so they can surface upstream
model ids ahead of OpenClaw’s static catalog.OAuth and usage endpoint providers
OAuth and usage endpoint providers
GitHub Copilot, Gemini CLI, ChatGPT Codex, MiniMax, Xiaomi, z.ai pair
prepareRuntimeAuth or formatApiKey with resolveUsageAuth +
fetchUsageSnapshot to own token exchange and /usage integration.Replay and transcript cleanup families
Replay and transcript cleanup families
Shared named families (
google-gemini, passthrough-gemini,
anthropic-by-model, hybrid-anthropic-openai) let providers opt into
transcript policy via buildReplayPolicy instead of each plugin
re-implementing cleanup.Catalog-only providers
Catalog-only providers
byteplus, cloudflare-ai-gateway, huggingface, kimi-coding, nvidia,
qianfan, synthetic, together, venice, vercel-ai-gateway, and
volcengine register just catalog and ride the shared inference loop.Anthropic-specific stream helpers
Anthropic-specific stream helpers
Beta headers,
/fast / serviceTier, and context1m live inside the
Anthropic plugin’s public api.ts / contract-api.ts seam
(wrapAnthropicProviderStream, resolveAnthropicBetas,
resolveAnthropicFastMode, resolveAnthropicServiceTier) rather than in
the generic SDK.Runtime helpers
Plugins can access selected core helpers viaapi.runtime. For TTS:
textToSpeechreturns the normal core TTS output payload for file/voice-note surfaces.- Uses core
messages.ttsconfiguration and provider selection. - Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
listVoicesis optional per provider. Use it for vendor-owned voice pickers or setup flows.- Voice listings can include richer metadata such as locale, gender, and personality tags for provider-aware pickers.
- OpenAI and ElevenLabs support telephony today. Microsoft does not.
api.registerSpeechProvider(...).
- Keep TTS policy, fallback, and reply delivery in core.
- Use speech providers for vendor-owned synthesis behavior.
- Legacy Microsoft
edgeinput is normalized to themicrosoftprovider id. - The preferred ownership model is company-oriented: one vendor plugin can own text, speech, image, and future media providers as OpenClaw adds those capability contracts.
- Keep orchestration, fallback, config, and channel wiring in core.
- Keep vendor behavior in the provider plugin.
- Additive expansion should stay typed: new optional methods, new optional result fields, new optional capabilities.
- Video generation already follows the same pattern:
- core owns the capability contract and runtime helper
- vendor plugins register
api.registerVideoGenerationProvider(...) - feature/channel plugins consume
api.runtime.videoGeneration.*
api.runtime.mediaUnderstanding.*is the preferred shared surface for image/audio/video understanding.- Uses core media-understanding audio configuration (
tools.media.audio) and provider fallback order. - Returns
{ text: undefined }when no transcription output is produced (for example skipped/unsupported input). api.runtime.stt.transcribeAudioFile(...)remains as a compatibility alias.
api.runtime.subagent:
providerandmodelare optional per-run overrides, not persistent session changes.- OpenClaw only honors those override fields for trusted callers.
- For plugin-owned fallback runs, operators must opt in with
plugins.entries.<id>.subagent.allowModelOverride: true. - Use
plugins.entries.<id>.subagent.allowedModelsto restrict trusted plugins to specific canonicalprovider/modeltargets, or"*"to allow any target explicitly. - Untrusted plugin subagent runs still work, but override requests are rejected instead of silently falling back.
api.registerWebSearchProvider(...).
Notes:
- Keep provider selection, credential resolution, and shared request semantics in core.
- Use web-search providers for vendor-specific search transports.
api.runtime.webSearch.*is the preferred shared surface for feature/channel plugins that need search behavior without depending on the agent tool wrapper.
api.runtime.imageGeneration
generate(...): generate an image using the configured image-generation provider chain.listProviders(...): list available image-generation providers and their capabilities.
Gateway HTTP routes
Plugins can expose HTTP endpoints withapi.registerHttpRoute(...).
path: route path under the gateway HTTP server.auth: required. Use"gateway"to require normal gateway auth, or"plugin"for plugin-managed auth/webhook verification.match: optional."exact"(default) or"prefix".replaceExisting: optional. Allows the same plugin to replace its own existing route registration.handler: returntruewhen the route handled the request.
api.registerHttpHandler(...)was removed and will cause a plugin-load error. Useapi.registerHttpRoute(...)instead.- Plugin routes must declare
authexplicitly. - Exact
path + matchconflicts are rejected unlessreplaceExisting: true, and one plugin cannot replace another plugin’s route. - Overlapping routes with different
authlevels are rejected. Keepexact/prefixfallthrough chains on the same auth level only. auth: "plugin"routes do not receive operator runtime scopes automatically. They are for plugin-managed webhooks/signature verification, not privileged Gateway helper calls.auth: "gateway"routes run inside a Gateway request runtime scope, but that scope is intentionally conservative:- shared-secret bearer auth (
gateway.auth.mode = "token"/"password") keeps plugin-route runtime scopes pinned tooperator.write, even if the caller sendsx-openclaw-scopes - trusted identity-bearing HTTP modes (for example
trusted-proxyorgateway.auth.mode = "none"on a private ingress) honorx-openclaw-scopesonly when the header is explicitly present - if
x-openclaw-scopesis absent on those identity-bearing plugin-route requests, runtime scope falls back tooperator.write
- shared-secret bearer auth (
- Practical rule: do not assume a gateway-auth plugin route is an implicit admin surface. If your route needs admin-only behavior, require an identity-bearing auth mode and document the explicit
x-openclaw-scopesheader contract.
Plugin SDK import paths
Use narrow SDK subpaths instead of the monolithicopenclaw/plugin-sdk root
barrel when authoring new plugins. Core subpaths:
| Subpath | Purpose |
|---|---|
openclaw/plugin-sdk/plugin-entry | Plugin registration primitives |
openclaw/plugin-sdk/channel-core | Channel entry/build helpers |
openclaw/plugin-sdk/core | Generic shared helpers and umbrella contract |
openclaw/plugin-sdk/config-schema | Root openclaw.json Zod schema (OpenClawSchema) |
channel-setup,
setup-runtime, setup-adapter-runtime, setup-tools, channel-pairing,
channel-contract, channel-feedback, channel-inbound, channel-lifecycle,
channel-reply-pipeline, command-auth, secret-input, webhook-ingress,
channel-targets, and channel-actions. Approval behavior should consolidate
on one approvalCapability contract rather than mixing across unrelated
plugin fields. See Channel plugins.
Runtime and config helpers live under matching *-runtime subpaths
(approval-runtime, config-runtime, infra-runtime, agent-runtime,
lazy-runtime, directory-runtime, text-runtime, runtime-store, etc.).
openclaw/plugin-sdk/channel-runtime is deprecated — a compatibility shim for
older plugins. New code should import narrower generic primitives instead.index.js— bundled plugin entryapi.js— helper/types barrelruntime-api.js— runtime-only barrelsetup-entry.js— setup plugin entry
openclaw/plugin-sdk/* subpaths. Never
import another plugin package’s src/* from core or from another plugin.
Facade-loaded entry points prefer the active runtime config snapshot when one
exists, then fall back to the resolved config file on disk.
Capability-specific subpaths such as image-generation, media-understanding,
and speech exist because bundled plugins use them today. They are not
automatically long-term frozen external contracts — check the relevant SDK
reference page when relying on them.
Message tool schemas
Plugins should own channel-specificdescribeMessageTool(...) schema
contributions for non-message primitives such as reactions, reads, and polls.
Shared send presentation should use the generic MessagePresentation contract
instead of provider-native button, component, block, or card fields.
See Message Presentation for the contract,
fallback rules, provider mapping, and plugin author checklist.
Send-capable plugins declare what they can render through message capabilities:
presentationfor semantic presentation blocks (text,context,divider,buttons,select)delivery-pinfor pinned-delivery requests
Channel target resolution
Channel plugins should own channel-specific target semantics. Keep the shared outbound host generic and use the messaging adapter surface for provider rules:messaging.inferTargetChatType({ to })decides whether a normalized target should be treated asdirect,group, orchannelbefore directory lookup.messaging.targetResolver.looksLikeId(raw, normalized)tells core whether an input should skip straight to id-like resolution instead of directory search.messaging.targetResolver.resolveTarget(...)is the plugin fallback when core needs a final provider-owned resolution after normalization or after a directory miss.messaging.resolveOutboundSessionRoute(...)owns provider-specific session route construction once a target is resolved.
- Use
inferTargetChatTypefor category decisions that should happen before searching peers/groups. - Use
looksLikeIdfor “treat this as an explicit/native target id” checks. - Use
resolveTargetfor provider-specific normalization fallback, not for broad directory search. - Keep provider-native ids like chat ids, thread ids, JIDs, handles, and room
ids inside
targetvalues or provider-specific params, not in generic SDK fields.
Config-backed directories
Plugins that derive directory entries from config should keep that logic in the plugin and reuse the shared helpers fromopenclaw/plugin-sdk/directory-runtime.
Use this when a channel needs config-backed peers/groups such as:
- allowlist-driven DM peers
- configured channel/group maps
- account-scoped static directory fallbacks
directory-runtime only handle generic operations:
- query filtering
- limit application
- deduping/normalization helpers
- building
ChannelDirectoryEntry[]
Provider catalogs
Provider plugins can define model catalogs for inference withregisterProvider({ catalog: { run(...) { ... } } }).
catalog.run(...) returns the same shape OpenClaw writes into
models.providers:
{ provider }for one provider entry{ providers }for multiple provider entries
catalog when the plugin owns provider-specific model ids, base URL
defaults, or auth-gated model metadata.
catalog.order controls when a plugin’s catalog merges relative to OpenClaw’s
built-in implicit providers:
simple: plain API-key or env-driven providersprofile: providers that appear when auth profiles existpaired: providers that synthesize multiple related provider entrieslate: last pass, after other implicit providers
discoverystill works as a legacy alias- if both
cataloganddiscoveryare registered, OpenClaw usescatalog
Read-only channel inspection
If your plugin registers a channel, prefer implementingplugin.config.inspectAccount(cfg, accountId) alongside resolveAccount(...).
Why:
resolveAccount(...)is the runtime path. It is allowed to assume credentials are fully materialized and can fail fast when required secrets are missing.- Read-only command paths such as
openclaw status,openclaw status --all,openclaw channels status,openclaw channels resolve, and doctor/config repair flows should not need to materialize runtime credentials just to describe configuration.
inspectAccount(...) behavior:
- Return descriptive account state only.
- Preserve
enabledandconfigured. - Include credential source/status fields when relevant, such as:
tokenSource,tokenStatusbotTokenSource,botTokenStatusappTokenSource,appTokenStatussigningSecretSource,signingSecretStatus
- You do not need to return raw token values just to report read-only
availability. Returning
tokenStatus: "available"(and the matching source field) is enough for status-style commands. - Use
configured_unavailablewhen a credential is configured via SecretRef but unavailable in the current command path.
Package packs
A plugin directory may include apackage.json with openclaw.extensions:
name/<fileBase>.
If your plugin imports npm deps, install them in that directory so
node_modules is available (npm install / pnpm install).
Security guardrail: every openclaw.extensions entry must stay inside the plugin
directory after symlink resolution. Entries that escape the package directory are
rejected.
Security note: openclaw plugins install installs plugin dependencies with
npm install --omit=dev --ignore-scripts (no lifecycle scripts, no dev dependencies at runtime). Keep plugin dependency
trees “pure JS/TS” and avoid packages that require postinstall builds.
Optional: openclaw.setupEntry can point at a lightweight setup-only module.
When OpenClaw needs setup surfaces for a disabled channel plugin, or
when a channel plugin is enabled but still unconfigured, it loads setupEntry
instead of the full plugin entry. This keeps startup and setup lighter
when your main plugin entry also wires tools, hooks, or other runtime-only
code.
Optional: openclaw.startup.deferConfiguredChannelFullLoadUntilAfterListen
can opt a channel plugin into the same setupEntry path during the gateway’s
pre-listen startup phase, even when the channel is already configured.
Use this only when setupEntry fully covers the startup surface that must exist
before the gateway starts listening. In practice, that means the setup entry
must register every channel-owned capability that startup depends on, such as:
- channel registration itself
- any HTTP routes that must be available before the gateway starts listening
- any gateway methods, tools, or services that must exist during that same window
singleAccountKeysToMovenamedAccountPromotionKeysresolveSingleAccountPromotionTarget(...)
channels.<id>.accounts.* without loading the full plugin entry.
Matrix is the current bundled example: it moves only auth/bootstrap keys into a
named promoted account when named accounts already exist, and it can preserve a
configured non-canonical default-account key instead of always creating
accounts.default.
Those setup patch adapters keep bundled contract-surface discovery lazy. Import
time stays light; the promotion surface is loaded only on first use instead of
re-entering bundled channel startup on module import.
When those startup surfaces include gateway RPC methods, keep them on a
plugin-specific prefix. Core admin namespaces (config.*,
exec.approvals.*, wizard.*, update.*) remain reserved and always resolve
to operator.admin, even if a plugin requests a narrower scope.
Example:
Channel catalog metadata
Channel plugins can advertise setup/discovery metadata viaopenclaw.channel and
install hints via openclaw.install. This keeps the core catalog data-free.
Example:
openclaw.channel fields beyond the minimal example:
detailLabel: secondary label for richer catalog/status surfacesdocsLabel: override link text for the docs linkpreferOver: lower-priority plugin/channel ids this catalog entry should outrankselectionDocsPrefix,selectionDocsOmitLabel,selectionExtras: selection-surface copy controlsmarkdownCapable: marks the channel as markdown-capable for outbound formatting decisionsexposure.configured: hide the channel from configured-channel listing surfaces when set tofalseexposure.setup: hide the channel from interactive setup/configure pickers when set tofalseexposure.docs: mark the channel as internal/private for docs navigation surfacesshowConfigured/showInSetup: legacy aliases still accepted for compatibility; preferexposurequickstartAllowFrom: opt the channel into the standard quickstartallowFromflowforceAccountBinding: require explicit account binding even when only one account existspreferSessionLookupForAnnounceTarget: prefer session lookup when resolving announce targets
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.json
OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) at
one or more JSON files (comma/semicolon/PATH-delimited). Each file should
contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }. The parser also accepts "packages" or "plugins" as legacy aliases for the "entries" key.
Context engine plugins
Context engine plugins own session context orchestration for ingest, assembly, and compaction. Register them from your plugin withapi.registerContextEngine(id, factory), then select the active engine with
plugins.slots.contextEngine.
Use this when your plugin needs to replace or extend the default context
pipeline rather than just add memory search or hooks.
compact()
implemented and delegate it explicitly:
Adding a new capability
When a plugin needs behavior that does not fit the current API, do not bypass the plugin system with a private reach-in. Add the missing capability. Recommended sequence:- define the core contract Decide what shared behavior core should own: policy, fallback, config merge, lifecycle, channel-facing semantics, and runtime helper shape.
- add typed plugin registration/runtime surfaces
Extend
OpenClawPluginApiand/orapi.runtimewith the smallest useful typed capability surface. - wire core + channel/feature consumers Channels and feature plugins should consume the new capability through core, not by importing a vendor implementation directly.
- register vendor implementations Vendor plugins then register their backends against the capability.
- add contract coverage Add tests so ownership and registration shape stay explicit over time.
Capability checklist
When you add a new capability, the implementation should usually touch these surfaces together:- core contract types in
src/<capability>/types.ts - core runner/runtime helper in
src/<capability>/runtime.ts - plugin API registration surface in
src/plugins/types.ts - plugin registry wiring in
src/plugins/registry.ts - plugin runtime exposure in
src/plugins/runtime/*when feature/channel plugins need to consume it - capture/test helpers in
src/test-utils/plugin-registration.ts - ownership/contract assertions in
src/plugins/contracts/registry.ts - operator/plugin docs in
docs/
Capability template
Minimal pattern:- core owns the capability contract + orchestration
- vendor plugins own vendor implementations
- feature/channel plugins consume runtime helpers
- contract tests keep ownership explicit
Related
- Plugin architecture — public capability model and shapes
- Plugin SDK subpaths
- Plugin SDK setup
- Building plugins