Plugins are just small code modules that extend OpenClaw with additional functionality (commands, tools, and Gateway RPC). Most of the time, you'll use plugins when you want a feature that isn't built into the core OpenClaw yet (or you want to keep optional features out of the main installation). Quick path:
openclaw plugins list
xxxxxxxxxxopenclaw plugins install @openclaw/voice-call
plugins.entries.<id>.config.@openclaw/msteams.plugins.slots.memory)plugins.slots.memory = "memory-lancedb")@openclaw/voice-call@openclaw/zalouser@openclaw/matrix@openclaw/nostr@openclaw/zalo@openclaw/msteamsgoogle-antigravity-auth (disabled by default)google-gemini-cli-auth (disabled by default)qwen-portal-auth (disabled by default)github-copilot device login (bundled, disabled by default)OpenClaw plugins are TypeScript modules loaded at runtime via jiti. Configuration validation does not execute plugin code; it uses the plugin manifest and JSON Schema. See Plugin Manifest. Plugins can register:
skills directory in plugin manifest)Plugins run in the same process as Gateway, so treat them as trusted code. Tool writing guide: Plugin Agent Tools.
Plugins can access selected core helpers via api.runtime. For telephony TTS:
xxxxxxxxxxconst result = await api.runtime.tts.textToSpeechTelephony({ text: "Hello from OpenClaw", cfg: api.config,});Notes:
messages.tts configuration (OpenAI or ElevenLabs).OpenClaw scans in order:
plugins.load.paths (files or directories)<workspace>/.openclaw/extensions/*.ts<workspace>/.openclaw/extensions/*/index.ts~/.openclaw/extensions/*.ts~/.openclaw/extensions/*/index.ts<openclaw>/extensions/*Bundled plugins must be explicitly enabled via plugins.entries.<id>.enabled or openclaw plugins enable <id>. Installed plugins are enabled by default but can be disabled the same way. Each plugin must contain an openclaw.plugin.json file in its root directory. If the path points to a file, the plugin root is the file's directory and must contain the manifest. If multiple plugins resolve to the same id, the first match in the above order wins, and lower-priority duplicates are ignored.
Plugin directories can contain a package.json with openclaw.extensions:
xxxxxxxxxx{ "name": "my-pack", "openclaw": { "extensions": ["./src/safety.ts", "./src/tools.ts"] }}Each entry becomes a plugin. If a package lists multiple extensions, the plugin id becomes name/<fileBase>. If your plugin imports npm dependencies, install them in that directory so node_modules is available (npm install / pnpm install).
Channel plugins can broadcast onboarding metadata via openclaw.channel and installation hints via openclaw.install. This keeps the core directory data-free. Example:
xxxxxxxxxx{ "name": "@openclaw/nextcloud-talk", "openclaw": { "extensions": ["./index.ts"], "channel": { "id": "nextcloud-talk", "label": "Nextcloud Talk", "selectionLabel": "Nextcloud Talk (self-hosted)", "docsPath": "/channels/nextcloud-talk", "docsLabel": "nextcloud-talk", "blurb": "Self-hosted chat via Nextcloud Talk webhook bots.", "order": 65, "aliases": ["nc-talk", "nc"] }, "install": { "npmSpec": "@openclaw/nextcloud-talk", "localPath": "extensions/nextcloud-talk", "defaultChoice": "npm" } }}OpenClaw can also merge external channel directories (e.g., MPM registry exports). Place JSON files at one of:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.jsonOr set OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) to one or more JSON files (comma/semicolon/PATH separated). Each file should contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }.
Default plugin id:
name from package.json~/.../voice-call.ts → voice-call)If a plugin exports id, OpenClaw will use it but will warn if it doesn't match the configured id.
xxxxxxxxxx{ plugins: { enabled: true, allow: ["voice-call"], deny: ["untrusted-plugin"], load: { paths: ["~/Projects/oss/voice-call-extension"] }, entries: { "voice-call": { enabled: true, config: { provider: "twilio" } }, }, },}Fields:
enabled: Main switch (default: true)allow: Allowlist (optional)deny: Denylist (optional; deny takes precedence)load.paths: Additional plugin files/directoriesentries.<id>: Per-plugin switches + configurationConfiguration changes require Gateway restart. Validation rules (strict):
entries, allow, deny, or slots are errors.channels.<id> keys are errors unless plugin manifest declares channel id.openclaw.plugin.json (configSchema).Some plugin categories are exclusive (only one active at a time). Use plugins.slots to choose which plugin owns the slot:
xxxxxxxxxx{ plugins: { slots: { memory: "memory-core", // or "none" to disable memory plugins }, },}If multiple plugins declare kind: "memory", only the selected one loads. Others are disabled with diagnostics.
Control interfaces use config.schema (JSON Schema + uiHints) to render better forms. OpenClaw enhances uiHints at runtime based on discovered plugins:
plugins.entries.<id> / .enabled / .configplugins.entries.<id>.config.<field>If you want plugin configuration fields to display good labels/placeholders (and mark secrets as sensitive), provide uiHints and JSON Schema in your plugin manifest. Example:
xxxxxxxxxx{ "id": "my-plugin", "configSchema": { "type": "object", "additionalProperties": false, "properties": { "apiKey": { "type": "string" }, "region": { "type": "string" } } }, "uiHints": { "apiKey": { "label": "API Key", "sensitive": true }, "region": { "label": "Region", "placeholder": "us-east-1" } }}xxxxxxxxxxopenclaw plugins listopenclaw plugins info <id>openclaw plugins install <path> # copy a local file/dir into ~/.openclaw/extensions/<id>openclaw plugins install ./extensions/voice-call # relative path okopenclaw plugins install ./plugin.tgz # install from a local tarballopenclaw plugins install ./plugin.zip # install from a local zipopenclaw plugins install -l ./extensions/voice-call # link (no copy) for devopenclaw plugins install @openclaw/voice-call # install from npmopenclaw plugins update <id>openclaw plugins update --allopenclaw plugins enable <id>openclaw plugins disable <id>openclaw plugins doctorplugins update only works for npm installations tracked under plugins.installs. Plugins can also register their own top-level commands (e.g., openclaw voicecall).
Plugins export one of:
(api) => { ... }{ id, name, configSchema, register(api) { ... } }Plugins can ship with hooks and register them at runtime. This lets plugins bundle event-driven automation without needing to install a separate hooks package.
ximport { registerPluginHooksFromDir } from "openclaw/plugin-sdk";export default function register(api) { registerPluginHooksFromDir(api, "./hooks");}Notes:
HOOK.md + handler.ts).plugin:<id> in openclaw hooks list.openclaw hooks; instead enable/disable the plugin.Plugins can register model provider authentication flows so users can run OAuth or API key setup within OpenClaw (without external scripts). Register providers via api.registerProvider(...). Each provider exposes one or more authentication methods (OAuth, API key, device code, etc.). These methods drive:
openclaw models auth login --provider <id> [--method <id>]Example:
xxxxxxxxxxapi.registerProvider({ id: "acme", label: "AcmeAI", auth: [ { id: "oauth", label: "OAuth", kind: "oauth", run: async (ctx) => { // Run OAuth flow and return auth profiles. return { profiles: [ { profileId: "acme:default", credential: { type: "oauth", provider: "acme", access: "...", refresh: "...", expires: Date.now() + 3600 * 1000, }, }, ], defaultModel: "acme/opus-1", }; }, }, ],});Notes:
run receives ProviderAuthContext with prompter, runtime, openUrl, and oauth.createVpsAwareHandlers helpers.configPatch when adding default model or provider configuration is needed.defaultModel so --set-default can update agent defaults.Plugins can register channel plugins, which behave like built-in channels (WhatsApp, Telegram, etc.). Channel configuration lives under channels.<id> and is validated by your channel plugin code.
xxxxxxxxxxconst myChannel = { id: "acmechat", meta: { id: "acmechat", label: "AcmeChat", selectionLabel: "AcmeChat (API)", docsPath: "/channels/acmechat", blurb: "demo channel plugin.", aliases: ["acme"], }, capabilities: { chatTypes: ["direct"] }, config: { listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}), resolveAccount: (cfg, accountId) => cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? { accountId, }, }, outbound: { deliveryMode: "direct", sendText: async () => ({ ok: true }), },};export default function (api) { api.registerChannel({ plugin: myChannel });}Notes:
channels.<id> (not plugins.entries).meta.label is used for labels in CLI/UI lists.meta.aliases add alternate ids for normalization and CLI input.meta.preferOver lists channel ids to skip auto-enabling when both are configured.meta.detailLabel and meta.systemImage let UI display richer channel labels/icons.Use this when you want a new chat interface ("message channel") rather than a model provider. Model provider documentation is under /providers/*.
channels.<id>.channels.<id>.accounts.<accountId>.meta.label, meta.selectionLabel, meta.docsPath, meta.blurb control CLI/UI lists.meta.docsPath should point to a documentation page like /channels/<id>.meta.preferOver lets plugin replace another channel (auto-enable preferring it).meta.detailLabel and meta.systemImage are used by UI for detailed text/icons.config.listAccountIds + config.resolveAccountcapabilities (chat types, media, threading, etc.)outbound.deliveryMode + outbound.sendText (for basic sending)setup (wizard), security (DM policy), status (health/diagnostics)gateway (start/stop/login), mentions, threading, streamingactions (message actions), commands (native command behavior)api.registerChannel({ plugin })Minimal configuration example:
xxxxxxxxxx{ channels: { acmechat: { accounts: { default: { token: "ACME_TOKEN", enabled: true }, }, }, },}Load the plugin (extensions directory or plugins.load.paths), restart Gateway, then configure channels.<id> in your configuration.
See dedicated guide: Plugin Agent Tools.
xxxxxxxxxxexport default function (api) { api.registerGatewayMethod("myplugin.status", ({ respond }) => { respond(true, { ok: true }); });}xxxxxxxxxxexport default function (api) { api.registerCli( ({ program }) => { program.command("mycmd").action(() => { console.log("Hello"); }); }, { commands: ["mycmd"] }, );}Plugins can register custom slash commands that execute without calling the AI agent. This is useful for toggle commands, status checks, or quick operations that don't need LLM processing.
xxxxxxxxxxexport default function (api) { api.registerCommand({ name: "mystatus", description: "Show plugin status", handler: (ctx) => ({ text: `Plugin is running! Channel: ${ctx.channel}`, }), });}Command handler context:
senderId: ID of the sender (if available)channel: Channel where the command was sentisAuthorizedSender: Whether the sender is an authorized userargs: Arguments passed after the command (if acceptsArgs: true)commandBody: Full command textconfig: Current OpenClaw configurationCommand options:
name: Command name (without leading /)description: Help text shown in command listacceptsArgs: Whether command accepts arguments (default: false). If false and arguments are provided, command won't match and message will be passed to other handlersrequireAuth: Whether authorized sender is required (default: true)handler: Function returning { text: string } (can be async)Example with authorization and arguments:
xxxxxxxxxxapi.registerCommand({ name: "setmode", description: "Set plugin mode", acceptsArgs: true, requireAuth: true, handler: async (ctx) => { const mode = ctx.args?.trim() || "default"; await saveMode(mode); return { text: `Mode set to: ${mode}` }; },});Notes:
/MyStatus matches /mystatus)help, status, reset, etc.) cannot be overridden by pluginsxxxxxxxxxxexport default function (api) { api.registerService({ id: "my-service", start: () => api.logger.info("ready"), stop: () => api.logger.info("bye"), });}pluginId.action (e.g., voicecall.status)snake_case (e.g., voice_call)Plugins can ship with Skills in their repo (skills/<name>/SKILL.md). Enable it using plugins.entries.<id>.enabled (or other configuration gating) and ensure it exists in your workspace/hosted Skills location.
Recommended packaging:
openclaw (this repo)@openclaw/* (e.g., @openclaw/voice-call)Publishing contract:
package.json must contain openclaw.extensions with one or more entry files..js or .ts (jiti loads TS at runtime).openclaw plugins install <npm-spec> uses npm pack, extracts to ~/.openclaw/extensions/<id>/, and enables it in configuration.plugins.entries.*.This repo contains a voice call plugin (Twilio or log fallback):
extensions/voice-callskills/voice-callopenclaw voicecall start|statusvoice_callvoicecall.start, voicecall.statusprovider: "twilio" + twilio.accountSid/authToken/from (optional statusCallbackUrl, twimlUrl)provider: "log" (no network)See Voice Call and extensions/voice-call/README.md for setup and usage.
Plugins run in the same process as Gateway. Treat them as trusted code:
plugins.allow allowlist.Plugins can (and should) ship with tests:
src/** (e.g., src/plugins/voice-call.plugin.test.ts).openclaw.extensions points to built entry point (dist/index.js).For more plugin content, refer to: Plugins - OpenClaw