---
summary: "Reference for defineToolPlugin, definePluginEntry, defineChannelPluginEntry, and defineSetupPluginEntry"
title: "Plugin entry points"
sidebarTitle: "Entry Points"
read_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 exports a default entry object. The SDK provides helpers for
creating them.

For installed plugins, `package.json` should point runtime loading at built
JavaScript when available:

```json
{
  "openclaw": {
    "extensions": ["./src/index.ts"],
    "runtimeExtensions": ["./dist/index.js"],
    "setupEntry": "./src/setup-entry.ts",
    "runtimeSetupEntry": "./dist/setup-entry.js"
  }
}
```

`extensions` and `setupEntry` remain valid source entries for workspace and git
checkout development. `runtimeExtensions` and `runtimeSetupEntry` are preferred
when OpenClaw loads an installed package and let npm packages avoid runtime
TypeScript compilation. Explicit runtime entries are required: `runtimeSetupEntry`
requires `setupEntry`, and missing `runtimeExtensions` or `runtimeSetupEntry`
artifacts fail install/discovery instead of silently falling back to source. If
an installed package only declares a TypeScript source entry, OpenClaw will use a
matching built `dist/*.js` peer when one exists, then fall back to the TypeScript
source.

All entry paths must stay inside the plugin package directory. Runtime entries
and inferred built JavaScript peers do not make an escaping `extensions` or
`setupEntry` source path valid.

<Tip>
  **Looking for a walkthrough?** See [Tool Plugins](/plugins/tool-plugins),
  [Channel Plugins](/plugins/sdk-channel-plugins), or
  [Provider Plugins](/plugins/sdk-provider-plugins) for step-by-step guides.
</Tip>

## `defineToolPlugin`

**Import:** `openclaw/plugin-sdk/tool-plugin`

For simple plugins that only add agent tools. `defineToolPlugin` keeps the
authoring 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.

```typescript
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. When omitted, OpenClaw uses a strict empty object
  schema and 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`.
- Tool names are static. `openclaw plugins build` derives `contracts.tools`
  from the declared tools, so authors do not duplicate names by hand.
- Runtime loading stays strict. Installed plugins still need
  `openclaw.plugin.json` and `package.json` `openclaw.extensions`; OpenClaw does
  not execute plugin code to infer missing manifest data.

## `definePluginEntry`

**Import:** `openclaw/plugin-sdk/plugin-entry`

For provider plugins, advanced tool plugins, hook plugins, and anything that is
**not** a messaging channel.

```typescript
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`                                                         | No       | -                   |
| `configSchema` | `OpenClawPluginConfigSchema \| () => OpenClawPluginConfigSchema` | No       | Empty object schema |
| `register`     | `(api: OpenClawPluginApi) => void`                               | Yes      | -                   |

- `id` must match your `openclaw.plugin.json` manifest.
- `kind` is for exclusive slots: `"memory"` or `"context-engine"`.
- `configSchema` can be a function for lazy evaluation.
- OpenClaw resolves and memoizes that schema on first access, so expensive schema
  builders only run once.

## `defineChannelPluginEntry`

**Import:** `openclaw/plugin-sdk/channel-core`

Wraps `definePluginEntry` with channel-specific wiring. Automatically calls
`api.registerChannel({ plugin })`, exposes an optional root-help CLI metadata
seam, and gates `registerFull` on registration mode.

```typescript
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       | -                   |

- `setRuntime` is called during registration so you can store the runtime reference
  (typically via `createPluginRuntimeStore`). It is skipped during CLI metadata
  capture.
- `registerCliMetadata` runs during `api.registrationMode === "cli-metadata"`,
  `api.registrationMode === "discovery"`, and
  `api.registrationMode === "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 command registration remains compatible with full plugin loads.
- Discovery registration is non-activating, not import-free. OpenClaw may
  evaluate the trusted plugin entry and channel plugin module to build the
  snapshot, so keep top-level imports side-effect-free and put sockets,
  clients, workers, and services behind `"full"`-only paths.
- `registerFull` only runs when `api.registrationMode === "full"`. It is skipped
  during setup-only loading.
- Like `definePluginEntry`, `configSchema` can be a lazy factory and OpenClaw
  memoizes the resolved schema on first access.
- For plugin-owned root CLI commands, prefer `api.registerCli(..., { descriptors: [...] })`
  when you want the command to stay lazy-loaded without disappearing from the
  root CLI parse tree. For paired-node feature commands, prefer
  `api.registerNodeCliFeature(...)` so the command lands under `openclaw 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, prefer
  registering those descriptors from `registerCliMetadata(...)` and keep
  `registerFull(...)` focused on runtime-only work.
- If `registerFull(...)` also registers gateway RPC methods, keep them on a
  plugin-specific prefix. Reserved core admin namespaces (`config.*`,
  `exec.approvals.*`, `wizard.*`, `update.*`) are always coerced to
  `operator.admin`.

## `defineSetupPluginEntry`

**Import:** `openclaw/plugin-sdk/channel-core`

For the lightweight `setup-entry.ts` file. Returns just `{ plugin }` with no
runtime or CLI wiring.

```typescript
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](/plugins/sdk-setup#setup-entry) for when this matters.

In practice, pair `defineSetupPluginEntry(...)` with the narrow setup helper
families:

- `openclaw/plugin-sdk/setup-runtime` for runtime-safe setup helpers such as
  `createSetupTranslator`, import-safe setup patch adapters, lookup-note output,
  `promptResolvedAllowFrom`, `splitSetupEntries`, and delegated setup proxies
- `openclaw/plugin-sdk/channel-setup` for optional-install setup surfaces
- `openclaw/plugin-sdk/setup-tools` for setup/install CLI/archive/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. That contract lets the
setup entry keep setup-safe plugin/secrets exports while still exposing a
runtime setter:

```typescript
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 that bundled contract only when setup flows truly need 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` 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 |
| `"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:

```typescript
register(api) {
  if (
    api.registrationMode === "cli-metadata" ||
    api.registrationMode === "discovery" ||
    api.registrationMode === "full"
  ) {
    api.registerCli(/* ... */);
    if (api.registrationMode === "cli-metadata") 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"`.

For CLI registrars specifically:

- use `descriptors` when the registrar owns one or more root commands and you
  want OpenClaw to lazy-load the real CLI module on first invocation
- make sure those descriptors cover every top-level command root exposed by the
  registrar
- keep descriptor command names to letters, numbers, hyphen, and underscore,
  starting with a letter or number; OpenClaw rejects descriptor names outside
  that shape and strips terminal control sequences from descriptions before
  rendering help
- use `commands` alone only for eager compatibility paths

## Plugin shapes

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.

## Related

- [SDK Overview](/plugins/sdk-overview) - registration API and subpath reference
- [Runtime Helpers](/plugins/sdk-runtime) - `api.runtime` and `createPluginRuntimeStore`
- [Setup and Config](/plugins/sdk-setup) - manifest, setup entry, deferred loading
- [Channel Plugins](/plugins/sdk-channel-plugins) - building the `ChannelPlugin` object
- [Provider Plugins](/plugins/sdk-provider-plugins) - provider registration and hooks
