Plugin Entry Points: defineToolPlugin, definePluginEntry, and More
Reference for the SDK helpers used to define plugin entry points: defineToolPlugin, definePluginEntry, defineChannelPluginEntry, and defineSetupPluginEntry. Covers package.json fields for source and runtime entries.
Read this when
- You need the exact type signature of defineToolPlugin, definePluginEntry, or defineChannelPluginEntry
- You want to understand registration mode (full vs setup vs CLI metadata)
- You are looking up entry point options
Each plugin must export a default entry object. The SDK supplies a dedicated helper for every entry type: defineToolPlugin, definePluginEntry, defineChannelPluginEntry, defineSetupPluginEntry.
Tip
Need a guided example? Check out Tool Plugins, Channel Plugins, or Provider Plugins for detailed walkthroughs.
Package entries
For installed plugins, the package.json and openclaw fields point to both source and built entries:
{
"openclaw": {
"extensions": ["./src/index.ts"],
"runtimeExtensions": ["./dist/index.js"],
"setupEntry": "./src/setup-entry.ts",
"runtimeSetupEntry": "./dist/setup-entry.js"
}
}
extensionsandsetupEntryare source entries, intended for workspace and git checkout development.runtimeExtensionsandruntimeSetupEntryare the recommended choice for installed packages, since they let npm packages avoid runtime TypeScript compilation.- When
runtimeExtensionsis provided, its array length must equal that ofextensions(entries are paired by position).runtimeSetupEntryis required ifsetupEntryis present. - Declaring a
runtimeExtensionsorruntimeSetupEntryartifact that is missing causes install or discovery to fail with a packaging error; OpenClaw never silently falls back to the source. Source fallback (described below) applies only when no runtime entry is declared at all. - When an installed package declares only a TypeScript source entry, OpenClaw searches for a matching built
dist/*.js(or.mjs/.cjs) peer and uses it; otherwise it falls back to the TypeScript source. - Every entry path must remain within the plugin package directory. Runtime entries and inferred built JS peers do not make an escaping
extensionsorsetupEntrysource path valid.
defineToolPlugin
Import: openclaw/plugin-sdk/tool-plugin
Intended for plugins that provide agent tools only. Keeps the source minimal, infers configuration and tool parameter types from TypeBox schemas, wraps plain return values into 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." }),
}),
outputSchema: Type.Object(
{
symbol: Type.String(),
hasKey: Type.Boolean(),
},
{ additionalProperties: false },
),
execute: async ({ symbol }, config) => ({ symbol, hasKey: Boolean(config.apiKey) }),
}),
],
});
configSchemais optional; when omitted, a strict empty object schema is used (the generated manifest still includesconfigSchema).executereturns either a plain string or a JSON serializable value; the helper wraps it as a text tool result withdetailsset to the original (unstringified) return value.outputSchemaoptionally describes that originaldetailsvalue for Code Mode and Tool Search. Catalog calls reject an invalid schema before execution and validate the final value before returning it.- For custom tool results,
openclaw/plugin-sdk/tool-resultsexportstextResultandjsonResult. - Tool names are static, so
openclaw plugins buildderivescontracts.toolsfrom the declared tools without manually duplicating names. - Runtime loading remains strict: installed plugins still require
openclaw.plugin.jsonandpackage.jsonopenclaw.extensions. OpenClaw never runs plugin code to infer missing manifest data.
definePluginEntry
Import: openclaw/plugin-sdk/plugin-entry
Designed 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 | - |
idhas to correspond with the value in youropenclaw.plugin.jsonmanifest.- For external session catalogs, use
openclaw/plugin-sdk/session-catalogtogether withapi.registerSessionCatalog({ id, label, list, read, continueSession?, archive? }). Thesessions.catalog.*Gateway methods are managed by Core; providers give back host, session, and normalized transcript projections without registering any RPCs. A list provider should invoke the optionalonHost(host)callback once each host has settled; the host array returned remains mandatory as the final compatibility snapshot. kindis deprecated: instead, define a dedicated slot ("memory"or"context-engine") inside theopenclaw.plugin.jsonmanifest'skindfield. The runtime-entrykindpersists only as a backward-compatibility shim for older plugins.configSchemamay be a function to enable lazy evaluation. OpenClaw resolves and caches the schema the first time it is accessed, so expensive schema constructors execute only once.- A
nodeHostCommandsdescriptor is able to defineisAvailable({ config, env }). Returningfalseremoves that command and its capability from the headless node's Gateway declaration. OpenClaw evaluates it against the node-local startup configuration; command handlers should still verify availability when called.
defineChannelPluginEntry
Import: openclaw/plugin-sdk/channel-core
Wraps definePluginEntry with channel-specific wiring: it automatically
invokes api.registerChannel({ plugin }), exposes an optional root-help CLI
metadata seam, and conditions 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 execute per registration mode (complete table under Registration mode):
setRuntimeis active in all modes except"cli-metadata"and"tool-discovery". Keep the runtime reference stored here, usually throughcreatePluginRuntimeStore.registerCliMetadataapplies to"cli-metadata","discovery", and"full". Treat it as the standard location for channel-owned CLI descriptors so root help remains non-activating, discovery snapshots contain static command metadata, and normal CLI registration works with full plugin loads.registerFullactivates only for"full"and"tool-discovery". For"tool-discovery"it replaces channel registration: OpenClaw bypassesregisterChannel/setRuntimeentirely and invokes onlyregisterFull, so any provider or tool registration your channel needs for standalone tool discovery or execution must go there, not in the usual channel setup.- Discovery registration is non-activating but not import-free: OpenClaw may evaluate the trusted plugin entry and channel plugin module to build the snapshot. Keep top-level imports side-effect-free and place sockets, clients, workers, and services behind
"full"-only paths. - Like
definePluginEntry,configSchemacan act as a lazy factory; OpenClaw caches the resolved schema on first access.
CLI registration:
- Use
api.registerCli(..., { descriptors: [...] })for plugin-owned root CLI commands you want lazy-loaded without disappearing from the root CLI parse tree. Descriptor names must consist of letters, numbers, hyphens, and underscores, starting with a letter or number; OpenClaw rejects other patterns and strips terminal control sequences from descriptions before rendering help. Cover every top-level command root the registrar exposes. Onlycommandsstays on the eager compatibility path. - Use
api.registerNodeCliFeature(...)for paired-node feature commands so they appear underopenclaw nodes(the same asregisterCli(registrar, { parentPath: ["nodes"], ... })). - For other nested plugin commands, add
parentPathand register commands on theprogramobject passed to the registrar; OpenClaw resolves it to the parent command before calling the plugin. - For channel plugins, register CLI descriptors from
registerCliMetadataand keepregisterFullfocused on runtime-only tasks. - If
registerFullalso registers gateway RPC methods, keep them on a plugin-specific prefix. Reserved core admin namespaces (config.*,exec.approvals.*,wizard.*,update.*) always coerce tooperator.admin.
defineSetupPluginEntry
Import: openclaw/plugin-sdk/channel-core
For the lightweight setup-entry.ts file. Returns only { 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 and 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.
Registration mode
api.registrationMode informs your plugin about 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 manages this separation automatically. When working with
definePluginEntry on a channel, you must handle mode detection yourself and note that
"tool-discovery" omits 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(/* ... */);
}
Services with extended lifetimes can emit small invalidation or lifecycle signals through their service context:
api.registerService({
id: "index-events",
start(ctx) {
ctx.gatewayEvents?.emit("changed", { revision: 1 }, { scope: "operator.read" });
},
});
OpenClaw designates this as plugin.<plugin-id>.changed. Event names consist of a single
lowercase segment, payloads must be bounded JSON, and the scope must be
operator.read, operator.write, or operator.admin. The emitter remains active only
during the service lifetime and gets revoked after a stop or failed start. Use
version or invalidation payloads rather than full records so authorized clients can re-read
canonical state through the plugin's scoped Gateway methods.
Discovery mode creates 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: avoid network clients, subprocesses, listeners, database connections, background workers, credential reads, or other live runtime side effects at the top level.
Treat "setup-runtime" as the window where setup-only startup surfaces must
exist without re-entering the full bundled channel runtime. Suitable use cases are
channel registration, setup-safe HTTP routes, setup-safe gateway methods,
and delegated setup helpers. Heavy background services, CLI registrars, and
provider or client SDK bootstraps still belong in "full".
Plugin shapes
OpenClaw categorizes loaded plugins based on 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 inspect a plugin's shape.
Related
- SDK Overview - registration API and subpath reference
- Runtime Helpers -
api.runtimeandcreatePluginRuntimeStore - Setup and Config - manifest, setup entry, deferred loading
- Channel Plugins - building the
ChannelPluginobject - Provider Plugins - provider registration and hooks