docs/plugins/sdk-setup.md
Reference for plugin packaging (package.json metadata), manifests (openclaw.plugin.json), setup entries, and config schemas.
Your package.json needs an openclaw field that tells the plugin system what your plugin provides:
openclaw fieldsopenclaw.channelopenclaw.channel is cheap package metadata for channel discovery and setup surfaces before runtime loads.
| Field | Type | What it means |
|---|---|---|
id | string | Canonical channel id. |
label | string | Primary channel label. |
selectionLabel | string | Picker/setup label when it should differ from label. |
detailLabel | string | Secondary detail label for richer channel catalogs and status surfaces. |
docsPath | string | Docs path for setup and selection links. |
docsLabel | string | Override label used for docs links when it should differ from the channel id. |
blurb | string | Short onboarding/catalog description. |
order | number | Sort order in channel catalogs. |
aliases | string[] | Extra lookup aliases for channel selection. |
preferOver | string[] | Lower-priority plugin/channel ids this channel should outrank. |
systemImage | string | Optional icon/system-image name for channel UI catalogs. |
selectionDocsPrefix | string | Prefix text before docs links in selection surfaces. |
selectionDocsOmitLabel | boolean | Show the docs path directly instead of a labeled docs link in selection copy. |
selectionExtras | string[] | Extra short strings appended in selection copy. |
markdownCapable | boolean | Marks the channel as markdown-capable for outbound formatting decisions. |
exposure | object | Channel visibility controls for setup, configured lists, and docs surfaces. |
quickstartAllowFrom | boolean | Opt this channel into the standard quickstart allowFrom setup flow. |
forceAccountBinding | boolean | Require explicit account binding even when only one account exists. |
preferSessionLookupForAnnounceTarget | boolean | Prefer session lookup when resolving announce targets for this channel. |
Example:
{
"openclaw": {
"channel": {
"id": "my-channel",
"label": "My Channel",
"selectionLabel": "My Channel (self-hosted)",
"detailLabel": "My Channel Bot",
"docsPath": "/channels/my-channel",
"docsLabel": "my-channel",
"blurb": "Webhook-based self-hosted chat integration.",
"order": 80,
"aliases": ["mc"],
"preferOver": ["my-channel-legacy"],
"selectionDocsPrefix": "Guide:",
"selectionExtras": ["Markdown"],
"markdownCapable": true,
"exposure": {
"configured": true,
"setup": true,
"docs": true
},
"quickstartAllowFrom": true
}
}
}
exposure supports:
configured: include the channel in configured/status-style listing surfacessetup: include the channel in interactive setup/configure pickersdocs: mark the channel as public-facing in docs/navigation surfacesopenclaw.installopenclaw.install is package metadata, not manifest metadata.
| Field | Type | What it means |
|---|---|---|
clawhubSpec | string | Canonical ClawHub spec for install/update and onboarding install-on-demand flows. |
npmSpec | string | Canonical npm spec for install/update fallback flows. |
localPath | string | Local development or bundled install path. |
defaultChoice | "clawhub" | "npm" | "local" | Preferred install source when multiple sources are available. |
minHostVersion | string | Minimum supported OpenClaw version in the form >=x.y.z or >=x.y.z-prerelease. |
expectedIntegrity | string | Expected npm dist integrity string, usually sha512-..., for pinned installs. |
allowInvalidConfigRecovery | boolean | Lets bundled-plugin reinstall flows recover from specific stale-config failures. |
```json
{
"openclaw": {
"install": {
"npmSpec": "@wecom/[email protected]",
"expectedIntegrity": "sha512-REPLACE_WITH_NPM_DIST_INTEGRITY",
"defaultChoice": "npm"
}
}
}
```
Channel plugins can opt into deferred loading with:
{
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"startup": {
"deferConfiguredChannelFullLoadUntilAfterListen": true
}
}
}
When enabled, OpenClaw loads only setupEntry during the pre-listen startup phase, even for already-configured channels. The full entry loads after the gateway starts listening.
If your setup/full entry registers gateway RPC methods, keep them on a plugin-specific prefix. Reserved core admin namespaces (config.*, exec.approvals.*, wizard.*, update.*) stay core-owned and always resolve to operator.admin.
Every native plugin must ship an openclaw.plugin.json in the package root. OpenClaw uses this to validate config without executing plugin code.
{
"id": "my-plugin",
"name": "My Plugin",
"description": "Adds My Plugin capabilities to OpenClaw",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"webhookSecret": {
"type": "string",
"description": "Webhook verification secret"
}
}
}
}
For channel plugins, add kind and channels:
{
"id": "my-channel",
"kind": "channel",
"channels": ["my-channel"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
Even plugins with no config must ship a schema. An empty schema is valid:
{
"id": "my-plugin",
"configSchema": {
"type": "object",
"additionalProperties": false
}
}
See Plugin manifest for the full schema reference.
For plugin packages, use the package-specific ClawHub command:
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
The setup-entry.ts file is a lightweight alternative to index.ts that OpenClaw loads when it only needs setup surfaces (onboarding, config repair, disabled channel inspection).
// setup-entry.ts
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { myChannelPlugin } from "./src/channel.js";
export default defineSetupPluginEntry(myChannelPlugin);
This avoids loading heavy runtime code (crypto libraries, CLI registrations, background services) during setup flows.
Bundled workspace channels that keep setup-safe exports in sidecar modules can use defineBundledChannelSetupEntry(...) from openclaw/plugin-sdk/channel-entry-contract instead of defineSetupPluginEntry(...). That bundled contract also supports an optional runtime export so setup-time runtime wiring can stay lightweight and explicit.
Those startup gateway methods should still avoid reserved core admin namespaces such as `config.*` or `update.*`.
For hot setup-only paths, prefer the narrow setup helper seams over the broader plugin-sdk/setup umbrella when you only need part of the setup surface:
| Import path | Use it for | Key exports |
|---|---|---|
plugin-sdk/setup-runtime | setup-time runtime helpers that stay available in setupEntry / deferred channel startup | createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator, noteChannelLookupFailure, noteChannelLookupSummary, promptResolvedAllowFrom, splitSetupEntries, createAllowlistSetupWizardProxy, createDelegatedSetupWizardProxy |
plugin-sdk/setup-adapter-runtime | environment-aware account setup adapters | createEnvPatchedAccountSetupAdapter |
plugin-sdk/setup-tools | setup/install CLI/archive/docs helpers | formatCliCommand, detectBinary, extractArchive, resolveBrewExecutable, formatDocsLink, CONFIG_DIR |
Use the broader plugin-sdk/setup seam when you want the full shared setup toolbox, including config-patch helpers such as moveSingleAccountChannelSectionToDefaultAccount(...).
The setup patch adapters stay hot-path safe on import. Their bundled single-account promotion contract-surface lookup is lazy, so importing plugin-sdk/setup-runtime does not eagerly load bundled contract-surface discovery before the adapter is actually used.
When a channel upgrades from a single-account top-level config to channels.<id>.accounts.*, the default shared behavior is to move promoted account-scoped values into accounts.default.
Bundled channels can narrow or override that promotion through their setup contract surface:
singleAccountKeysToMove: extra top-level keys that should move into the promoted accountnamedAccountPromotionKeys: when named accounts already exist, only these keys move into the promoted account; shared policy/delivery keys stay at the channel rootresolveSingleAccountPromotionTarget(...): choose which existing account receives promoted valuesPlugin config is validated against the JSON Schema in your manifest. Users configure plugins via:
{
plugins: {
entries: {
"my-plugin": {
config: {
webhookSecret: "abc123",
},
},
},
},
}
Your plugin receives this config as api.pluginConfig during registration.
For channel-specific config, use the channel config section instead:
{
channels: {
"my-channel": {
token: "bot-token",
allowFrom: ["user1", "user2"],
},
},
}
Use buildChannelConfigSchema to convert a Zod schema into the ChannelConfigSchema wrapper used by plugin-owned config artifacts:
import { z } from "zod";
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
const accountSchema = z.object({
token: z.string().optional(),
allowFrom: z.array(z.string()).optional(),
accounts: z.object({}).catchall(z.any()).optional(),
defaultAccount: z.string().optional(),
});
const configSchema = buildChannelConfigSchema(accountSchema);
If you already author the contract as JSON Schema or TypeBox, use the direct helper so OpenClaw can skip Zod-to-JSON-Schema conversion on metadata paths:
import { Type } from "typebox";
import { buildJsonChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
const configSchema = buildJsonChannelConfigSchema(
Type.Object({
token: Type.Optional(Type.String()),
allowFrom: Type.Optional(Type.Array(Type.String())),
}),
);
For third-party plugins, the cold-path contract is still the plugin manifest: mirror the generated JSON Schema into openclaw.plugin.json#channelConfigs so config schema, setup, and UI surfaces can inspect channels.<id> without loading runtime code.
Channel plugins can provide interactive setup wizards for openclaw onboard. The wizard is a ChannelSetupWizard object on the ChannelPlugin:
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/channel-setup";
const setupWizard: ChannelSetupWizard = {
channel: "my-channel",
status: {
configuredLabel: "Connected",
unconfiguredLabel: "Not configured",
resolveConfigured: ({ cfg }) => Boolean((cfg.channels as any)?.["my-channel"]?.token),
},
credentials: [
{
inputKey: "token",
providerHint: "my-channel",
credentialLabel: "Bot token",
preferredEnvVar: "MY_CHANNEL_BOT_TOKEN",
envPrompt: "Use MY_CHANNEL_BOT_TOKEN from environment?",
keepPrompt: "Keep current token?",
inputPrompt: "Enter your bot token:",
inspect: ({ cfg, accountId }) => {
const token = (cfg.channels as any)?.["my-channel"]?.token;
return {
accountConfigured: Boolean(token),
hasConfiguredValue: Boolean(token),
};
},
},
],
};
The ChannelSetupWizard type supports credentials, textInputs, dmPolicy, allowFrom, groupAccess, prepare, finalize, and more. See bundled plugin packages (for example the Discord plugin src/channel.setup.ts) for full examples.
```typescript
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
const setupSurface = createOptionalChannelSetupSurface({
channel: "my-channel",
label: "My Channel",
npmSpec: "@myorg/openclaw-my-channel",
docsPath: "/channels/my-channel",
});
// Returns { setupAdapter, setupWizard }
```
`plugin-sdk/channel-setup` also exposes the lower-level `createOptionalChannelSetupAdapter(...)` and `createOptionalChannelSetupWizard(...)` builders when you only need one half of that optional-install surface.
The generated optional adapter/wizard fail closed on real config writes. They reuse one install-required message across `validateInput`, `applyAccountConfig`, and `finalize`, and append a docs link when `docsPath` is set.
- `createDetectedBinaryStatus(...)` for status blocks that vary only by labels, hints, scores, and binary detection
- `createCliPathTextInput(...)` for path-backed text inputs
- `createDelegatedSetupWizardStatusResolvers(...)`, `createDelegatedPrepare(...)`, `createDelegatedFinalize(...)`, and `createDelegatedResolveConfigured(...)` when `setupEntry` needs to forward to a heavier full wizard lazily
- `createDelegatedTextInputShouldPrompt(...)` when `setupEntry` only needs to delegate a `textInputs[*].shouldPrompt` decision
External plugins: publish to ClawHub, then install:
<Tabs> <Tab title="npm"> ```bash openclaw plugins install @myorg/openclaw-my-plugin ```Bare package specs install from npm during the launch cutover.
```bash
openclaw plugins install npm:@myorg/openclaw-my-plugin
```
In-repo plugins: place under the bundled plugin workspace tree and they are automatically discovered during build.
Users can install:
openclaw plugins install <package-name>
Bundled package metadata is explicit, not inferred from built JavaScript at gateway startup. Runtime dependencies belong in the plugin package that owns them; packaged OpenClaw startup never repairs or mirrors plugin dependencies.
definePluginEntry and defineChannelPluginEntry