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"
  }
}
  • extensions and setupEntry serve as source entries, ideal for development via workspace or git checkout.
  • When dealing with installed packages, runtimeExtensions and runtimeSetupEntry are the go-to options, as they allow npm packages to bypass TypeScript compilation at runtime.
  • If runtimeExtensions is included, its array length must equal that of extensions, since entries are paired by position. runtimeSetupEntry is a prerequisite for setupEntry.
  • Declaring a runtimeExtensions or runtimeSetupEntry artifact 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 .mjs or .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 extensions or setupEntry source 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) }),
    }),
  ],
});
  • configSchema is not required; leaving it out defaults to a strict empty object schema, though the generated manifest still includes configSchema.
  • execute yields a plain string or a JSON-serializable value, and the helper packages it as a text tool result with details holding the original, non-stringified return value.
  • outputSchema optionally provides a description of that original details value 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-results exports both textResult and jsonResult.
  • Since tool names are static, openclaw plugins build derives contracts.tools from the declared tools, avoiding any hand-written name duplication.
  • Runtime loading remains strict: installed plugins still require openclaw.plugin.json and package.json openclaw.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({/* ... */});
  },
});
FieldTypeRequiredDefault
idstringYes-
namestringYes-
descriptionstringYes-
kindstring (deprecated, see below)No-
configSchemaOpenClawPluginConfigSchema | () => OpenClawPluginConfigSchemaNoEmpty object schema
reloadOpenClawPluginReloadRegistrationNo-
nodeHostCommandsOpenClawPluginNodeHostCommand[]No-
securityAuditCollectorsOpenClawPluginSecurityAuditCollector[]No-
register(api: OpenClawPluginApi) => voidYes-
  • The value of id has to align with what your openclaw.plugin.json manifest declares.

  • For external session catalogs, rely on openclaw/plugin-sdk/session-catalog and set up a SessionCatalogProvider through api.registerSessionCatalog(...). The mandatory provider fields are id, label, list, and read; the optional hooks are resolveCreateSession, continueSession, checkUpstreamActivity, archive, openTerminal, and startTerminalSession. Core takes charge of the sessions.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 optional onHost(host) callback; the returned host array stays required as the final compatibility snapshot. Before OpenClaw advertises creation or triggers startTerminalSession, 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, use api.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 exact cwd, plus optional env, pathEnv, and title) or a paired-node plan (kind: "node", nodeId, command, paramsJSON, and the exact cwd). The sessions.catalog.startTerminal RPC demands operator.admin along with gateway.cliAgents.enabled and gateway.terminal.enabled. The caller handles provisioning of cwd; 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.

  • kind is no longer recommended. Use a dedicated slot ("memory" or "context-engine") in the openclaw.plugin.json manifest kind field instead. The runtime-entry kind persists solely as a fallback for compatibility with older plugins.

  • For lazy evaluation, configSchema can be a function. OpenClaw resolves and caches the schema on first access, ensuring expensive schema builders execute only once.

  • A nodeHostCommands descriptor may define isAvailable({ config, env }). Returning false excludes 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(/* ... */);
  },
});
FieldTypeRequiredDefault
idstringYes-
namestringYes-
descriptionstringYes-
pluginChannelPluginYes-
configSchemaOpenClawPluginConfigSchema | () => OpenClawPluginConfigSchemaNoEmpty object schema
setRuntime(runtime: PluginRuntime) => voidNo-
registerCliMetadata(api: OpenClawPluginApi) => voidNo-
registerFull(api: OpenClawPluginApi) => voidNo-
registerCapabilities(api: OpenClawPluginApi) => voidNo-

Callbacks execute according to registration mode (full table under Registration mode):

  • setRuntime is active in all modes apart from "cli-metadata" and "tool-discovery". Keep the runtime reference stored here, usually through createPluginRuntimeStore.
  • registerCliMetadata applies 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.
  • registerFull executes solely for "full" and "tool-discovery". In the case of "tool-discovery", it runs in place of channel registration: OpenClaw bypasses registerChannel/setRuntime completely and invokes the full-runtime callback, then the capability callback. Put tool registration in registerFull and capability providers in registerCapabilities.
  • registerCapabilities handles "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, configSchema may 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. Only commands remains 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 using getRootOptionAwareCommandPath from openclaw/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 under openclaw nodes (equivalent to registerCli(registrar, { parentPath: ["nodes"], ... })).
  • For other nested plugin commands, add parentPath and register commands on the program object passed to the registrar; OpenClaw resolves it to the parent command before calling the plugin.
  • For channel plugins, register CLI descriptors from registerCliMetadata and keep registerFull limited to runtime-only tasks.
  • If 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.

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:

ImportUse for
openclaw/plugin-sdk/setup-runtimeRuntime-safe setup helpers: createSetupTranslator, import-safe setup patch adapters, lookup-note output, promptResolvedAllowFrom, splitSetupEntries, delegated setup proxies
openclaw/plugin-sdk/channel-setupOptional-install setup surfaces
openclaw/plugin-sdk/channel-dm-policyAccount-aware DM policy descriptors for setup flows
openclaw/plugin-sdk/setup-toolsSetup/install CLI, archive, and docs helpers
openclaw/plugin-sdk/archiveBounded archive extraction and single-entry reads
openclaw/plugin-sdk/root-walkBudgeted, root-bounded directory walking
openclaw/plugin-sdk/secret-filePinned 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:

ModeWhenWhat to register
"full"Normal gateway startupEverything
"discovery"Read-only capability discoveryChannel registration, static CLI descriptors, and inert providers; skip sockets, workers, clients, and services
"tool-discovery"Scoped load to list or run specific plugins' toolsCapability/tool registration only; no channel activation
"setup-only"Disabled/unconfigured channelChannel registration only
"setup-runtime"Setup flow with runtime availableChannel registration plus only the lightweight runtime needed during setup
"cli-metadata"Root help / CLI metadata captureCLI 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:

ShapeDescription
plain-capabilityOne capability type (e.g. provider-only)
hybrid-capabilityMultiple capability types (e.g. provider + speech)
hook-onlyOnly hooks, no capabilities
non-capabilityTools/commands/services but no capabilities

Check a plugin's shape with openclaw plugins inspect <id>.

2,516 words · updated Aug 17, 2026