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"
  }
}
  • extensions and setupEntry are source entries, intended for workspace and git checkout development.
  • runtimeExtensions and runtimeSetupEntry are the recommended choice for installed packages, since they let npm packages avoid runtime TypeScript compilation.
  • When runtimeExtensions is provided, its array length must equal that of extensions (entries are paired by position). runtimeSetupEntry is required if setupEntry is present.
  • Declaring a runtimeExtensions or runtimeSetupEntry artifact 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 extensions or setupEntry source 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) }),
    }),
  ],
});
  • configSchema is optional; when omitted, a strict empty object schema is used (the generated manifest still includes configSchema).
  • execute returns either a plain string or a JSON serializable value; the helper wraps it as a text tool result with details set to the original (unstringified) return value.
  • outputSchema optionally describes that original details value 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-results exports textResult and jsonResult.
  • Tool names are static, so openclaw plugins build derives contracts.tools from the declared tools without manually duplicating names.
  • Runtime loading remains strict: installed plugins still require openclaw.plugin.json and package.json openclaw.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({/* ... */});
  },
});
FieldTypeRequiredDefault
idstringYes-
namestringYes-
descriptionstringYes-
kindstring (deprecated, see below)No-
configSchemaOpenClawPluginConfigSchema | () => OpenClawPluginConfigSchemaNoEmpty object schema
reloadOpenClawPluginReloadRegistrationNo-
nodeHostCommandsOpenClawPluginNodeHostCommand[]No-
securityAuditCollectorsOpenClawPluginSecurityAuditCollector[]No-
register(api: OpenClawPluginApi) => voidYes-
  • id has to correspond with the value in your openclaw.plugin.json manifest.
  • For external session catalogs, use openclaw/plugin-sdk/session-catalog together with api.registerSessionCatalog({ id, label, list, read, continueSession?, archive? }). The sessions.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 optional onHost(host) callback once each host has settled; the host array returned remains mandatory as the final compatibility snapshot.
  • kind is deprecated: instead, define a dedicated slot ("memory" or "context-engine") inside the openclaw.plugin.json manifest's kind field. The runtime-entry kind persists only as a backward-compatibility shim for older plugins.
  • configSchema may 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 nodeHostCommands descriptor is able to define isAvailable({ config, env }). Returning false removes 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(/* ... */);
  },
});
FieldTypeRequiredDefault
idstringYes-
namestringYes-
descriptionstringYes-
pluginChannelPluginYes-
configSchemaOpenClawPluginConfigSchema | () => OpenClawPluginConfigSchemaNoEmpty object schema
setRuntime(runtime: PluginRuntime) => voidNo-
registerCliMetadata(api: OpenClawPluginApi) => voidNo-
registerFull(api: OpenClawPluginApi) => voidNo-

Callbacks execute per registration mode (complete table under Registration mode):

  • setRuntime is active in all modes except "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 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.
  • registerFull activates only for "full" and "tool-discovery". For "tool-discovery" it replaces channel registration: OpenClaw bypasses registerChannel/setRuntime entirely and invokes only registerFull, 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, configSchema can 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. Only commands stays on the eager compatibility path.
  • Use api.registerNodeCliFeature(...) for paired-node feature commands so they appear under openclaw nodes (the same as 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 focused on 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, unconfigured, or when deferred loading is enabled. 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/setup-toolsSetup/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:

ModeWhenWhat to register
"full"Normal gateway startupEverything
"discovery"Read-only capability discoveryChannel 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' 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 before the full entry loads
"cli-metadata"Root help / CLI metadata captureCLI 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:

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

Use openclaw plugins inspect <id> to inspect a plugin's shape.