Plugin Entry Points: defineToolPlugin, definePluginEntry, and More
Reference for the SDK helpers that define plugin entry shapes: defineToolPlugin, definePluginEntry, defineChannelPluginEntry, and defineSetupPluginEntry. Covers package entry configuration for source and runtime builds.
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
Every plugin exposes a default entry object by default. For each entry shape, the SDK ships a dedicated helper: defineToolPlugin, definePluginEntry, defineChannelPluginEntry, and defineSetupPluginEntry.
Tip
Need a step-by-step guide? Check out Tool Plugins, Channel Plugins, or Provider Plugins for walkthroughs.
Package entries
For installed plugins, the package.json and openclaw fields point to both the source and the built versions of the entry:
{
"openclaw": {
"extensions": ["./src/index.ts"],
"runtimeExtensions": ["./dist/index.js"],
"setupEntry": "./src/setup-entry.ts",
"runtimeSetupEntry": "./dist/setup-entry.js"
}
}
extensionsandsetupEntryserve as source entries, ideal for development via workspace or git checkout.- When dealing with installed packages,
runtimeExtensionsandruntimeSetupEntryare the go-to options, as they allow npm packages to bypass TypeScript compilation at runtime. - If
runtimeExtensionsis included, its array length must equal that ofextensions, since entries are paired by position.runtimeSetupEntryis a prerequisite forsetupEntry. - Declaring a
runtimeExtensionsorruntimeSetupEntryartifact that turns out to be missing triggers a packaging error during install or discovery; OpenClaw won't quietly switch to the source. The source fallback described below only kicks in when no runtime entry is declared whatsoever. - When an installed package lists only a TypeScript source entry, OpenClaw searches for a corresponding built
dist/*.js(or.mjsor.cjs) peer and uses that; if none is found, it reverts to the TypeScript source. - Every entry path must remain within the plugin package directory. A runtime entry or an inferred built-JS peer does not make an escaping
extensionsorsetupEntrysource path acceptable.
defineToolPlugin
Import: openclaw/plugin-sdk/tool-plugin
Designed for plugins that solely add agent tools. It keeps the source compact, infers config 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 records in 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 not required; leaving it out defaults to a strict empty object schema, though the generated manifest still includesconfigSchema.executeyields a plain string or a JSON-serializable value, and the helper packages it as a text tool result withdetailsholding the original, non-stringified return value.outputSchemaoptionally provides a description of that originaldetailsvalue for Code Mode and Tool Search. Catalog calls reject an invalid schema before running and validate the final value prior to returning it.- For custom tool results,
openclaw/plugin-sdk/tool-resultsexports bothtextResultandjsonResult. - Since tool names are static,
openclaw plugins buildderivescontracts.toolsfrom the declared tools, avoiding any hand-written name duplication. - Runtime loading remains strict: installed plugins still require
openclaw.plugin.jsonandpackage.jsonopenclaw.extensions. OpenClaw never executes plugin code to fill in missing manifest details.
definePluginEntry
Import: 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 | - |
-
The value of
idhas to align with what youropenclaw.plugin.jsonmanifest declares. -
For external session catalogs, rely on
openclaw/plugin-sdk/session-catalogand set up aSessionCatalogProviderthroughapi.registerSessionCatalog(...). The mandatory provider fields areid,label,list, andread; the optional hooks areresolveCreateSession,continueSession,checkUpstreamActivity,archive,openTerminal, andstartTerminalSession. Core takes charge of thesessions.catalog.*Gateway methods; providers supply host, session, transcript, and terminal-plan projections without registering RPCs. When each host settles, a list provider should invoke the optionalonHost(host)callback; the returned host array stays required as the final compatibility snapshot. Before OpenClaw advertises creation or triggersstartTerminalSession,resolveCreateSession({ agentId })has to produce a config-derived model/runtime target. To apply the host's runtime and model-allowlist policy instead of duplicating it, useapi.runtime.agent.resolveSessionCatalogCreateTarget(...).A fresh CLI terminal plan is generated by
startTerminalSession({ agentId, cwd, initialMessage?, nodeId? }). You can return either a local plan (kind: "local",argv, and the exactcwd, plus optionalenv,pathEnv, andtitle) or a paired-node plan (kind: "node",nodeId,command,paramsJSON, and the exactcwd). Thesessions.catalog.startTerminalRPC demandsoperator.adminalong withgateway.cliAgents.enabledandgateway.terminal.enabled. The caller handles provisioning ofcwd; the Gateway requires an existing absolute local directory, rejects a changed plan cwd or host, and applies the normal agent-sandbox, node-pairing, deadline, and connection-ownership checks before opening the PTY. -
kindis no longer recommended. Use a dedicated slot ("memory"or"context-engine") in theopenclaw.plugin.jsonmanifestkindfield instead. The runtime-entrykindpersists solely as a fallback for compatibility with older plugins. -
For lazy evaluation,
configSchemacan be a function. OpenClaw resolves and caches the schema on first access, ensuring expensive schema builders execute only once. -
A
nodeHostCommandsdescriptor may defineisAvailable({ config, env }). Returningfalseexcludes that command and its capability from the headless node's Gateway declaration. OpenClaw checks it against the node-local startup config; command handlers should still verify availability when invoked.
Computer Use providers
Import: openclaw/plugin-sdk/computer-use
Node-local Computer Use plugins register a single provider through
registerComputerUseProvider(api, provider). The helper manages the
screen.snapshot and dangerous computer.act command registrations and the
matching Gateway invoke policy; the provider handles availability, execution,
serialization, frame state, driver lifecycle, and cleanup.
This same entry point exports the canonical TypeBox schemas, static types, and compiled validators for the two command payloads and the snapshot result. A node host accepts one provider for the command pair; registering another provider conflicts with the existing command registration rather than forming a fallback stack.
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 gates capability and full-runtime callbacks 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(/* ... */);
},
registerCapabilities(api) {
api.registerTranscriptSourceProvider(/* ... */);
},
});
| 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 | - |
registerCapabilities | (api: OpenClawPluginApi) => void | No | - |
Callbacks execute according to registration mode (full table under Registration mode):
setRuntimeis active in all modes apart from"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 spot for channel-specific CLI descriptors, so that root help stays non-activating, discovery snapshots carry static command metadata, and standard CLI registration works with complete plugin loads.registerFullexecutes solely for"full"and"tool-discovery". In the case of"tool-discovery", it runs in place of channel registration: OpenClaw bypassesregisterChannel/setRuntimecompletely and invokes the full-runtime callback, then the capability callback. Put tool registration inregisterFulland capability providers inregisterCapabilities.registerCapabilitieshandles"discovery","full", and"tool-discovery". Register inert advertised providers here so that read-only capability discovery can locate them without launching sockets, clients, workers, or services.- Discovery registration is non-activating but not import-free: OpenClaw may assess the trusted plugin entry and channel plugin module to assemble the snapshot. Ensure top-level imports have no side effects, and place sockets, clients, workers, and services behind paths that only use
"full". - Much like
definePluginEntry,configSchemamay act as a lazy factory; OpenClaw caches the resolved schema after the first access.
CLI registration:
- Apply
api.registerCli(..., { descriptors: [...] })for plugin-owned root CLI commands you want lazy-loaded without vanishing from the root CLI parse tree. Descriptor names must use letters, numbers, hyphen, and underscore, beginning with a letter or number; OpenClaw rejects other formats and removes terminal control sequences from descriptions before rendering help. Include every top-level command root the registrar exposes. Onlycommandsremains on the eager compatibility path. - Root descriptors can define a synchronous, pure
machineOutput({ argv, stdoutIsTTY })resolver for JSON, JSONL, or other machine-readable stdout modes not chosen solely by--json. Parse command tokens usinggetRootOptionAwareCommandPathfromopenclaw/plugin-sdk/cli-argv. Keep the resolver within lightweight CLI metadata and share it with full registration. Nested descriptors do not expose this field. - Use
api.registerNodeCliFeature(...)for paired-node feature commands so they appear underopenclaw nodes(equivalent toregisterCli(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 keepregisterFulllimited to 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 or unconfigured. 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/channel-dm-policy | Account-aware DM policy descriptors for setup flows |
openclaw/plugin-sdk/setup-tools | Setup/install CLI, archive, and docs helpers |
openclaw/plugin-sdk/archive | Bounded archive extraction and single-entry reads |
openclaw/plugin-sdk/root-walk | Budgeted, root-bounded directory walking |
openclaw/plugin-sdk/secret-file | Pinned secret reads and first-writer-wins creation |
Keep heavy SDKs, CLI registration, and long-lived runtime services in the full entry.
Bundled workspace channels that keep setup and runtime surfaces separate can swap in
defineBundledChannelSetupEntry(...) from
openclaw/plugin-sdk/channel-entry-contract instead. With it, the setup
entry keeps setup-safe plugin and secrets exports while a runtime setter remains
available:
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 */
},
});
},
});
Reach for this only when a setup flow genuinely needs a minimal runtime setter or
a setup-safe gateway surface for a channel that has not been configured.
registerSetupRuntime executes exclusively for "setup-runtime" loads; restrict it
to config-only routes or methods that the setup flow requires.
Registration mode
api.registrationMode reports to your plugin how it was loaded:
| Mode | When | What to register |
|---|---|---|
"full" | Normal gateway startup | Everything |
"discovery" | Read-only capability discovery | Channel registration, static CLI descriptors, and inert providers; 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 during setup |
"cli-metadata" | Root help / CLI metadata capture | CLI descriptors only |
defineChannelPluginEntry takes care of this split on its own. When calling
definePluginEntry directly for a channel, inspect the mode 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(/* ... */);
}
Long-lived services can push small invalidation or lifecycle events through their service context:
api.registerService({
id: "index-events",
start(ctx) {
ctx.gatewayEvents?.emit("changed", { revision: 1 }, { scope: "operator.read" });
},
});
OpenClaw labels this as plugin.<plugin-id>.changed. Event names use a single
lowercase segment, payloads must be bounded JSON, and the scope has to be
operator.read, operator.write, or operator.admin. The emitter lives only as long as
the service does and gets revoked after stop or failed start. Favor version or
invalidation payloads over full records so authorized clients reread canonical
state via the plugin's scoped Gateway methods.
Discovery mode constructs a registry snapshot without activating anything. It may still run the plugin entry and the channel plugin object so OpenClaw can register channel capabilities and static CLI descriptors. Module evaluation during discovery should be treated as trusted but minimal: 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".
Plugin shapes
OpenClaw groups 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 |
Check a plugin's shape with openclaw plugins inspect <id>.
Related
- SDK Overview - registration API and subpath reference
- Runtime Helpers -
api.runtimeandcreatePluginRuntimeStore - Setup and Config - manifest and setup entry loading
- Channel Plugins - building the
ChannelPluginobject - Provider Plugins - provider registration and hooks