docs/plugins/sdk-entrypoints.md
Every plugin exports a default entry object. The SDK provides a helper for
each entry shape: defineToolPlugin, definePluginEntry,
defineChannelPluginEntry, defineSetupPluginEntry.
Installed plugins point package.json openclaw fields at both source and
built entries:
{
"openclaw": {
"extensions": ["./src/index.ts"],
"runtimeExtensions": ["./dist/index.js"],
"setupEntry": "./src/setup-entry.ts",
"runtimeSetupEntry": "./dist/setup-entry.js"
}
}
extensions and setupEntry are source entries, used for workspace and git
checkout development.runtimeExtensions and runtimeSetupEntry are preferred for installed
packages: they let npm packages skip runtime TypeScript compilation.runtimeExtensions, when present, must match extensions in array length
(entries pair positionally). runtimeSetupEntry requires setupEntry.runtimeExtensions/runtimeSetupEntry artifact is declared but
missing, install/discovery fails with a packaging error; OpenClaw does not
silently fall back to source. Source fallback (below) only applies when no
runtime entry is declared at all.dist/*.js (or .mjs/.cjs) peer and uses it;
otherwise it falls back to the TypeScript source.extensions or
setupEntry source path valid.defineToolPluginImport: openclaw/plugin-sdk/tool-plugin
For plugins that only add agent tools. Keeps the source small, infers config
and tool-parameter types from TypeBox schemas, wraps plain return values in
the OpenClaw tool-result format, and exposes static metadata that
openclaw plugins build writes into the plugin manifest (contracts.tools,
configSchema).
import { Type } from "typebox";
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
export default defineToolPlugin({
id: "stock-quotes",
name: "Stock Quotes",
description: "Fetch stock quotes.",
configSchema: Type.Object({
apiKey: Type.Optional(Type.String({ description: "API key." })),
}),
tools: (tool) => [
tool({
name: "quote",
label: "Quote",
description: "Fetch a quote.",
parameters: Type.Object({
symbol: Type.String({ description: "Ticker symbol." }),
}),
execute: async ({ symbol }, config) => ({ symbol, hasKey: Boolean(config.apiKey) }),
}),
],
});
configSchema is optional; omitting it uses a strict empty object schema
(the generated manifest still includes configSchema).execute returns a plain string or JSON-serializable value; the helper
wraps it as a text tool result with details set to the original
(unstringified) return value.openclaw/plugin-sdk/tool-results exports
textResult and jsonResult.openclaw plugins build derives
contracts.tools from the declared tools without hand-duplicated names.openclaw.plugin.json and package.json openclaw.extensions. OpenClaw
never executes plugin code to infer missing manifest data.definePluginEntryImport: openclaw/plugin-sdk/plugin-entry
For provider plugins, advanced tool plugins, hook plugins, and anything that is not a messaging channel.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
export default definePluginEntry({
id: "my-plugin",
name: "My Plugin",
description: "Short summary",
register(api) {
api.registerProvider({
/* ... */
});
api.registerTool({
/* ... */
});
},
});
| Field | Type | Required | Default |
|---|---|---|---|
id | string | Yes | - |
name | string | Yes | - |
description | string | Yes | - |
kind | string (deprecated, see below) | No | - |
configSchema | OpenClawPluginConfigSchema | () => OpenClawPluginConfigSchema | No | Empty object schema |
reload | OpenClawPluginReloadRegistration | No | - |
nodeHostCommands | OpenClawPluginNodeHostCommand[] | No | - |
securityAuditCollectors | OpenClawPluginSecurityAuditCollector[] | No | - |
register | (api: OpenClawPluginApi) => void | Yes | - |
id must match your openclaw.plugin.json manifest.kind is deprecated: declare an exclusive slot ("memory" or
"context-engine") in the openclaw.plugin.json manifest kind field
instead. Runtime-entry kind remains only as a compatibility fallback for
older plugins.configSchema can be a function for lazy evaluation. OpenClaw resolves and
memoizes the schema on first access, so expensive schema builders only run
once.defineChannelPluginEntryImport: openclaw/plugin-sdk/channel-core
Wraps definePluginEntry with channel-specific wiring: it automatically
calls api.registerChannel({ plugin }), exposes an optional root-help CLI
metadata seam, and gates registerFull on registration mode.
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";
export default defineChannelPluginEntry({
id: "my-channel",
name: "My Channel",
description: "Short summary",
plugin: myChannelPlugin,
setRuntime: setMyRuntime,
registerCliMetadata(api) {
api.registerCli(/* ... */);
},
registerFull(api) {
api.registerGatewayMethod(/* ... */);
},
});
| Field | Type | Required | Default |
|---|---|---|---|
id | string | Yes | - |
name | string | Yes | - |
description | string | Yes | - |
plugin | ChannelPlugin | Yes | - |
configSchema | OpenClawPluginConfigSchema | () => OpenClawPluginConfigSchema | No | Empty object schema |
setRuntime | (runtime: PluginRuntime) => void | No | - |
registerCliMetadata | (api: OpenClawPluginApi) => void | No | - |
registerFull | (api: OpenClawPluginApi) => void | No | - |
Callbacks run per registration mode (full table under Registration mode):
setRuntime runs in every mode except "cli-metadata" and
"tool-discovery". Store the runtime reference here, typically via
createPluginRuntimeStore.registerCliMetadata runs for "cli-metadata", "discovery", and
"full". Use it as the canonical place for channel-owned CLI descriptors
so root help stays non-activating, discovery snapshots include static
command metadata, and normal CLI registration stays compatible with full
plugin loads.registerFull runs only for "full" and "tool-discovery". For
"tool-discovery" it runs instead of channel registration: OpenClaw
skips registerChannel/setRuntime entirely and calls only
registerFull, so any provider/tool registration your channel needs for
standalone tool discovery or execution must live there, not behind normal
channel setup."full"-only paths.definePluginEntry, configSchema can be a lazy factory; OpenClaw
memoizes the resolved schema on first access.CLI registration:
api.registerCli(..., { descriptors: [...] }) for plugin-owned root
CLI commands you want lazy-loaded without disappearing from the root CLI
parse tree. Descriptor names must match letters, numbers, hyphen, and
underscore, starting with a letter or number; OpenClaw rejects other
shapes and strips terminal control sequences from descriptions before
rendering help. Cover every top-level command root the registrar exposes.
commands alone stays on the eager compatibility path.api.registerNodeCliFeature(...) for paired-node feature commands so
they land under openclaw nodes (equivalent to
registerCli(registrar, { parentPath: ["nodes"], ... })).parentPath and register commands
on the program object passed to the registrar; OpenClaw resolves it to
the parent command before calling the plugin.registerCliMetadata
and keep registerFull focused on runtime-only work.registerFull also registers gateway RPC methods, keep them on a
plugin-specific prefix. Reserved core admin namespaces (config.*,
exec.approvals.*, wizard.*, update.*) always coerce to
operator.admin.defineSetupPluginEntryImport: openclaw/plugin-sdk/channel-core
For the lightweight setup-entry.ts file. Returns just { plugin } with no
runtime or CLI wiring.
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
export default defineSetupPluginEntry(myChannelPlugin);
OpenClaw loads this instead of the full entry when a channel is disabled, unconfigured, or when deferred loading is enabled. See Setup and Config for when this matters.
Pair defineSetupPluginEntry(...) with the narrow setup helper families:
| Import | Use for |
|---|---|
openclaw/plugin-sdk/setup-runtime | Runtime-safe setup helpers: createSetupTranslator, import-safe setup patch adapters, lookup-note output, promptResolvedAllowFrom, splitSetupEntries, delegated setup proxies |
openclaw/plugin-sdk/channel-setup | Optional-install setup surfaces |
openclaw/plugin-sdk/setup-tools | Setup/install CLI, archive, and docs helpers |
Keep heavy SDKs, CLI registration, and long-lived runtime services in the full entry.
Bundled workspace channels that split setup and runtime surfaces can use
defineBundledChannelSetupEntry(...) from
openclaw/plugin-sdk/channel-entry-contract instead. It lets the setup
entry keep setup-safe plugin/secrets exports while still exposing a runtime
setter:
import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
export default defineBundledChannelSetupEntry({
importMetaUrl: import.meta.url,
plugin: {
specifier: "./channel-plugin-api.js",
exportName: "myChannelPlugin",
},
runtime: {
specifier: "./runtime-api.js",
exportName: "setMyChannelRuntime",
},
registerSetupRuntime(api) {
api.registerHttpRoute({
path: "/my-channel/events",
auth: "plugin",
handler: async (req, res) => {
/* setup-safe route */
},
});
},
});
Use this only when a setup flow truly needs a lightweight runtime setter or
setup-safe gateway surface before the full channel entry loads.
registerSetupRuntime runs only for "setup-runtime" loads; keep it
limited to config-only routes or methods that must exist before deferred
full activation.
api.registrationMode tells your plugin how it was loaded:
| Mode | When | What to register |
|---|---|---|
"full" | Normal gateway startup | Everything |
"discovery" | Read-only capability discovery | Channel registration plus static CLI descriptors; entry code may load, but skip sockets, workers, clients, and services |
"tool-discovery" | Scoped load to list or run specific plugins' tools | Capability/tool registration only; no channel activation |
"setup-only" | Disabled/unconfigured channel | Channel registration only |
"setup-runtime" | Setup flow with runtime available | Channel registration plus only the lightweight runtime needed before the full entry loads |
"cli-metadata" | Root help / CLI metadata capture | CLI descriptors only |
defineChannelPluginEntry handles this split automatically. If you use
definePluginEntry directly for a channel, check mode yourself and remember
"tool-discovery" skips channel registration:
register(api) {
if (
api.registrationMode === "cli-metadata" ||
api.registrationMode === "discovery" ||
api.registrationMode === "full"
) {
api.registerCli(/* ... */);
if (api.registrationMode === "cli-metadata") return;
}
if (api.registrationMode === "tool-discovery") {
// Register capability-only surfaces (providers/tools), no channel.
return;
}
api.registerChannel({ plugin: myPlugin });
if (api.registrationMode !== "full") return;
// Heavy runtime-only registrations
api.registerService(/* ... */);
}
Discovery mode builds a non-activating registry snapshot. It may still evaluate the plugin entry and the channel plugin object so OpenClaw can register channel capabilities and static CLI descriptors. Treat module evaluation in discovery as trusted but lightweight: no network clients, subprocesses, listeners, database connections, background workers, credential reads, or other live runtime side effects at top level.
Treat "setup-runtime" as the window where setup-only startup surfaces must
exist without re-entering the full bundled channel runtime. Good fits are
channel registration, setup-safe HTTP routes, setup-safe gateway methods,
and delegated setup helpers. Heavy background services, CLI registrars, and
provider/client SDK bootstraps still belong in "full".
OpenClaw classifies loaded plugins by their registration behavior:
| Shape | Description |
|---|---|
| plain-capability | One capability type (e.g. provider-only) |
| hybrid-capability | Multiple capability types (e.g. provider + speech) |
| hook-only | Only hooks, no capabilities |
| non-capability | Tools/commands/services but no capabilities |
Use openclaw plugins inspect <id> to see a plugin's shape.
api.runtime and createPluginRuntimeStoreChannelPlugin object