* refactor(channels): move single-account promotion keys to plugin declarations * fix(channels): keep legacy promotion tier for undeclared setup adapters * fix(channels): keep plugin discovery lazy in setup promotion helpers * fix(channels): resolve bundled promotion surfaces from setup-only artifacts * test(matrix): use vi.stubEnv in setup test env helper
27 KiB
summary, title, sidebarTitle, read_when
| summary | title | sidebarTitle | read_when | |||
|---|---|---|---|---|---|---|
| Setup wizards, setup-entry.ts, config schemas, and package.json metadata | Plugin setup and config | Setup and config |
|
Reference for plugin packaging (package.json metadata), manifests (openclaw.plugin.json), setup entries, and config schemas.
Package metadata
Your package.json needs an openclaw field that tells the plugin system what your plugin provides:
openclaw fields
Entry point files (relative to package root). Valid source entries for workspace and git checkout development.
Built JavaScript peers for `extensions`, preferred when OpenClaw loads an installed npm package. See [SDK entry points](/plugins/sdk-entrypoints) for the source/built resolution order.
Lightweight setup-only entry (optional).
Built JavaScript peer for `setupEntry`. Requires `setupEntry` to also be set.
`{ id, label }` fallback plugin identity, used when a plugin has no channel/provider metadata to derive an id or label from.
Channel catalog metadata for setup, picker, quickstart, and status surfaces.
Install hints: `npmSpec`, `localPath`, `defaultChoice`, `minHostVersion`, `expectedIntegrity`, `allowInvalidConfigRecovery`, `requiredPlatformPackages`.
Startup behavior flags.
`pluginApi` version range this plugin supports. Required for external ClawHub publishes.
Provider ids (`providers: string[]`) are manifest metadata, not package metadata. Declare them in `openclaw.plugin.json`, not here — see [Plugin manifest](/plugins/manifest).
openclaw.channel
openclaw.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 surfaces
openclaw.install
openclaw.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, >=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. |
requiredPlatformPackages |
string[] |
Required platform-specific npm aliases verified during npm install. |
```json
{
"openclaw": {
"install": {
"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3",
"expectedIntegrity": "sha512-REPLACE_WITH_NPM_DIST_INTEGRITY",
"defaultChoice": "npm"
}
}
}
```
Deferred full load
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 normalize to operator.admin.
Plugin manifest
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 channels (and provider plugins add providers):
{
"id": "my-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.
ClawHub publishing
Skills and plugin packages use separate ClawHub publish commands. For plugin packages, use the package-specific command:
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
Setup entry
setup-entry.ts 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.*`.
Narrow setup helper imports
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 |
createSetupTranslator, createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator, noteChannelLookupFailure, noteChannelLookupSummary, promptResolvedAllowFrom, splitSetupEntries, createAllowlistSetupWizardProxy, createDelegatedSetupWizardProxy |
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(...).
Use createSetupTranslator(...) for fixed setup wizard copy. It uses the first nonblank value from OPENCLAW_LOCALE, LC_ALL, LC_MESSAGES, and LANG, in that order, then falls back to English. Set OPENCLAW_LOCALE=en for an explicit English override. Keep plugin-specific setup text in plugin-owned code and use shared catalog keys only for common setup labels, status text, and official bundled plugin setup copy.
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.
Channel-owned single-account promotion
When a channel upgrades from a single-account top-level config to channels.<id>.accounts.*, the default shared behavior moves promoted account-scoped values into accounts.default.
Every channel plugin can extend or narrow that promotion through its setup adapter:
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 values
The presence of singleAccountKeysToMove marks the promotion contract complete. Declare the field even when it is an empty array to opt out of legacy key promotion. Adapters that omit the field retain the pre-declaration promotion tiers for compatibility with already-published plugins; this compatibility tier is scheduled for removal at the next Plugin SDK major after the migration documented in #112238.
Declare openclaw.setupFeatures.configPromotion: true in the plugin package manifest when doctor must load these declarations from the lightweight bundled setup artifact. The setup-only plugin surface and the full channel plugin must expose the same declarations.
When calling moveSingleAccountChannelSectionToDefaultAccount(...) with an already resolved plugin, pass its setup adapter as setupSurface. Caller-supplied setup surfaces take precedence over loaded and bundled lookup, which keeps scoped or setup-only plugins independent of global registration.
Config schema
Plugin 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"],
},
},
}
Building channel config schemas
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.
Setup wizards
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),
};
},
},
],
};
ChannelSetupWizard also supports textInputs, dmPolicy, allowFrom, groupAccess, prepare, finalize, and more. See the Discord plugin's src/setup-core.ts for a full bundled example.
```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
Publishing and installing
External plugins: publish to ClawHub, then install:
```bash openclaw plugins install @myorg/openclaw-my-plugin ```Bare package specs install from npm during the launch cutover, unless the name matches a bundled or official plugin id, in which case OpenClaw uses that local/official copy instead. Use `clawhub:`, `npm:`, `git:`, or `npm-pack:` for deterministic source selection — see [Manage plugins](/plugins/manage-plugins).
```bash
openclaw plugins install npm:@myorg/openclaw-my-plugin
```
In-repo plugins: place under the bundled plugin workspace tree; they are automatically discovered during build.
For npm-sourced installs, `openclaw plugins install` installs the package into a per-plugin project under `~/.openclaw/npm/projects` with lifecycle scripts disabled (`--ignore-scripts`). Keep plugin dependency trees pure JS/TS and avoid packages that require `postinstall` builds. Gateway startup does not install plugin dependencies. npm/git/ClawHub install flows own dependency convergence; local plugins must already have their dependencies installed.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.
Related
- Building plugins — step-by-step getting started guide
- Plugin manifest — full manifest schema reference
- SDK entry points —
definePluginEntryanddefineChannelPluginEntry