Skip to main content
Reference for plugin packaging (package.json metadata), manifests (openclaw.plugin.json), setup entries, and config schemas.
Looking for a walkthrough? The how-to guides cover packaging in context: Channel plugins and Provider plugins.

Package metadata

Your package.json needs an openclaw field that tells the plugin system what your plugin provides:
{
  "name": "@myorg/openclaw-my-channel",
  "version": "1.0.0",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts"],
    "setupEntry": "./setup-entry.ts",
    "channel": {
      "id": "my-channel",
      "label": "My Channel",
      "blurb": "Short description of the channel."
    }
  }
}
Publishing externally on ClawHub requires compat and build. Canonical publish snippets live in docs/snippets/plugin-publish/.

openclaw fields

extensions
string[]
Entry point files (relative to package root). Valid source entries for workspace and git checkout development.
runtimeExtensions
string[]
Built JavaScript peers for extensions, preferred when OpenClaw loads an installed npm package. See SDK entry points for the source/built resolution order.
setupEntry
string
Lightweight setup-only entry (optional).
runtimeSetupEntry
string
Built JavaScript peer for setupEntry. Requires setupEntry to also be set.
plugin
object
{ id, label } fallback plugin identity, used when a plugin has no channel/provider metadata to derive an id or label from.
channel
object
Channel catalog metadata for setup, picker, quickstart, and status surfaces.
install
object
Install hints: npmSpec, localPath, defaultChoice, minHostVersion, expectedIntegrity, allowInvalidConfigRecovery, requiredPlatformPackages.
startup
object
Startup behavior flags.
compat
object
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.

openclaw.channel

openclaw.channel is cheap package metadata for channel discovery and setup surfaces before runtime loads.
FieldTypeWhat it means
idstringCanonical channel id.
labelstringPrimary channel label.
selectionLabelstringPicker/setup label when it should differ from label.
detailLabelstringSecondary detail label for richer channel catalogs and status surfaces.
docsPathstringDocs path for setup and selection links.
docsLabelstringOverride label used for docs links when it should differ from the channel id.
blurbstringShort onboarding/catalog description.
ordernumberSort order in channel catalogs.
aliasesstring[]Extra lookup aliases for channel selection.
preferOverstring[]Lower-priority plugin/channel ids this channel should outrank.
systemImagestringOptional icon/system-image name for channel UI catalogs.
selectionDocsPrefixstringPrefix text before docs links in selection surfaces.
selectionDocsOmitLabelbooleanShow the docs path directly instead of a labeled docs link in selection copy.
selectionExtrasstring[]Extra short strings appended in selection copy.
markdownCapablebooleanMarks the channel as markdown-capable for outbound formatting decisions.
exposureobjectChannel visibility controls for setup, configured lists, and docs surfaces.
quickstartAllowFrombooleanOpt this channel into the standard quickstart allowFrom setup flow.
forceAccountBindingbooleanRequire explicit account binding even when only one account exists.
preferSessionLookupForAnnounceTargetbooleanPrefer 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 surfaces
  • setup: include the channel in interactive setup/configure pickers
  • docs: mark the channel as public-facing in docs/navigation surfaces
showConfigured and showInSetup remain supported as legacy aliases. Prefer exposure.

openclaw.install

openclaw.install is package metadata, not manifest metadata.
FieldTypeWhat it means
clawhubSpecstringCanonical ClawHub spec for install/update and onboarding install-on-demand flows.
npmSpecstringCanonical npm spec for install/update fallback flows.
localPathstringLocal development or bundled install path.
defaultChoice"clawhub" | "npm" | "local"Preferred install source when multiple sources are available.
minHostVersionstringMinimum supported OpenClaw version, >=x.y.z or >=x.y.z-prerelease.
expectedIntegritystringExpected npm dist integrity string, usually sha512-..., for pinned installs.
allowInvalidConfigRecoverybooleanLets bundled-plugin reinstall flows recover from specific stale-config failures.
requiredPlatformPackagesstring[]Required platform-specific npm aliases verified during npm install.
Interactive onboarding uses openclaw.install for install-on-demand surfaces: if your plugin exposes provider auth choices or channel setup/catalog metadata before runtime loads, onboarding can prompt for ClawHub, npm, or local install, install or enable the plugin, then continue the selected flow. ClawHub choices use clawhubSpec and are preferred when present; npm choices require trusted catalog metadata with a registry npmSpec (exact versions and expectedIntegrity are optional pins, enforced on install/update when set). Keep “what to show” in openclaw.plugin.json and “how to install it” in package.json.
If minHostVersion is set, install and non-bundled manifest-registry loading both enforce it. Older hosts skip external plugins; invalid version strings are rejected. Bundled source plugins are assumed to be co-versioned with the host checkout.
For pinned npm installs, keep the exact version in npmSpec and add the expected artifact integrity:
{
  "openclaw": {
    "install": {
      "npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3",
      "expectedIntegrity": "sha512-REPLACE_WITH_NPM_DIST_INTEGRITY",
      "defaultChoice": "npm"
    }
  }
}
allowInvalidConfigRecovery is not a general bypass for broken configs. It is narrow bundled-plugin recovery only, letting reinstall/setup repair known upgrade leftovers like a missing bundled plugin path or a stale channels.<id> entry for that same plugin. If config is broken for unrelated reasons, install still fails closed and tells the operator to run openclaw doctor --fix.

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.
Only enable deferred loading when your setupEntry registers everything the gateway needs before it starts listening (channel registration, HTTP routes, gateway methods). If the full entry owns required startup capabilities, keep the default behavior.
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
clawhub skill publish <path> is a different command for publishing a skill folder, not a plugin package. See Publishing on ClawHub.

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.
  • The channel is disabled but needs setup/onboarding surfaces.
  • The channel is enabled but unconfigured.
  • Deferred loading is enabled (deferConfiguredChannelFullLoadUntilAfterListen).
  • The channel plugin object (via defineSetupPluginEntry).
  • Any HTTP routes required before gateway listen.
  • Any gateway methods needed during startup.
Those startup gateway methods should still avoid reserved core admin namespaces such as config.* or update.*.
  • CLI registrations.
  • Background services.
  • Heavy runtime imports (crypto, SDKs).
  • Gateway methods only needed after startup.

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 pathUse it forKey exports
plugin-sdk/setup-runtimesetup-time runtime helpers that stay available in setupEntry / deferred channel startupcreateSetupTranslator, createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator, noteChannelLookupFailure, noteChannelLookupSummary, promptResolvedAllowFrom, splitSetupEntries, createAllowlistSetupWizardProxy, createDelegatedSetupWizardProxy
plugin-sdk/setup-adapter-runtimedeprecated compatibility alias; use plugin-sdk/setup-runtimecreateEnvPatchedAccountSetupAdapter
plugin-sdk/setup-toolssetup/install CLI/archive/docs helpersformatCliCommand, 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 follows the CLI wizard locale (OPENCLAW_LOCALE, then system locale variables) and falls back to English. 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. Bundled channels can narrow or override that promotion through their setup contract surface:
  • singleAccountKeysToMove: extra top-level keys that should move into the promoted account
  • namedAccountPromotionKeys: when named accounts already exist, only these keys move into the promoted account; shared policy/delivery keys stay at the channel root
  • resolveSingleAccountPromotionTarget(...): choose which existing account receives promoted values
Matrix is the current bundled example. If exactly one named Matrix account already exists, or if defaultAccount points at an existing non-canonical key such as Ops, promotion preserves that account instead of creating a new accounts.default entry.

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.
For DM allowlist prompts that only need the standard note -> prompt -> parse -> merge -> patch flow, prefer the shared setup helpers from openclaw/plugin-sdk/setup: createPromptParsedAllowFromForAccount(...), createTopLevelChannelParsedAllowFromPrompt(...), and createNestedChannelParsedAllowFromPrompt(...).
For channel setup status blocks that only vary by labels, scores, and optional extra lines, prefer createStandardChannelSetupStatus(...) from openclaw/plugin-sdk/setup instead of hand-rolling the same status object in each plugin.
For optional setup surfaces that should only appear in certain contexts, use createOptionalChannelSetupSurface from openclaw/plugin-sdk/channel-setup:
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.
For binary-backed setup UIs, prefer the shared delegated helpers instead of copying the same binary/status glue into every channel:
  • 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:
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.
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.