A plugin is simply a small code module that extends OpenClaw with additional functionality (commands, tools, and Gateway RPCs). Most of the time, you will use plugins when you require a feature that is not yet built into the core OpenClaw system (or when you wish to keep optional features separate from the main installation). Quick steps:
openclaw plugins list
xxxxxxxxxxopenclaw plugins install @openclaw/voice-call
plugins.entries.<id>.config.
@openclaw/msteams. - Memory (Core) — Bundled memory search plugin (enabled by default via 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 relies on the plugin manifest and JSON Schema. See Plugin Manifest. Plugins can register:
skills directory in the plugin manifest)Plugins run within the same process as the Gateway; therefore, treat them as trusted code. Tool Authoring Guide: Plugin Agent Tools.
Plugins can access selected core helper utilities 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 the following 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 in the same manner. Each plugin must include an openclaw.plugin.json file in its root directory. If a path points to a specific file, the plugin's root directory is the directory containing that file, which must also contain the manifest. If multiple plugins resolve to the same ID, the first match encountered in the order listed above takes precedence, and lower-priority duplicates are ignored.
A plugin directory can contain a package.json file featuring an openclaw.extensions field:
xxxxxxxxxx{"name": "my-pack","openclaw": {"extensions": ["./src/safety.ts", "./src/tools.ts"]}}
Each entry in this list becomes a distinct plugin. If a package lists multiple extensions, the plugin ID takes the format name/<fileBase>. If your plugins import npm dependencies, please install them within that directory to ensure the node_modules are available (npm install / pnpm install).
Channel plugins can broadcast onboarding metadata via openclaw.channel and installation prompts via openclaw.install. This approach keeps the core directory free of data. 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 catalogs (e.g., MPM registry exports). Place the JSON file in one of the following locations:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.jsonAlternatively, set OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) to point to one or more JSON files (separated by commas, semicolons, or PATH separators). Each file should contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }.
Default Plugin ID:
name field in package.json~/.../voice-call.ts → voice-call)If a plugin exports an id, OpenClaw will use it; however, it will issue a warning if this ID does not 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: Master switch (Default: true)allow: Allowlist (Optional)deny: Denylist (Optional; deny takes precedence)load.paths: Additional plugin files/directoriesentries.<id>: Per-plugin switch + configurationConfiguration changes require a Gateway restart. Validation Rules (Strict):
entries, allow, deny, or slots constitute an error.channels.<id> keys constitute an error, unless the plugin manifest declares the channel ID.openclaw.plugin.json (configSchema).Certain plugin categories are exclusive (only one can be active at a time). Use plugins.slots to select which plugin occupies a given slot:
xxxxxxxxxx{plugins: {slots: {memory: "memory-core", // or "none" to disable memory plugins},},}
If multiple plugins declare kind: "memory", only the selected one is loaded. The others are disabled and accompanied by diagnostic information.
The control interface utilizes config.schema (JSON Schema + uiHints) to render enhanced forms. At runtime, OpenClaw augments the uiHints based on the plugins it discovers:
plugins.entries.<id>, .enabled, and .config.plugins.entries.<id>.config.<field>.If you wish for your plugin's configuration fields to display user-friendly labels and placeholders (and to mark specific keys as sensitive), please provide both uiHints and a JSON Schema within 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 doctor
plugins update applies only to npm installations tracked under plugins.installs. Plugins may also register their own top-level commands (e.g., openclaw voicecall).
Plugins export one of the following:
(api) => { ... }{ id, name, configSchema, register(api) { ... } }Plugins can include hooks and register them at runtime. This allows plugins to bundle event-driven automations without the need to install separate hook packages.
ximport { registerPluginHooksFromDir } from "openclaw/plugin-sdk";export default function register(api) {registerPluginHooksFromDir(api, "./hooks");}
Notes:
HOOK.md + handler.ts).plugin:<id> in the openclaw hooks list output.openclaw hooks; instead, you enable or disable the plugin itself.Plugins can register model provider authentication workflows, allowing users to complete OAuth or API key setup directly within OpenClaw (without the need for external scripts). Providers are registered via api.registerProvider(...). Each provider exposes one or more authentication methods (OAuth, API keys, device codes, etc.). These methods power the following command:
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 a ProviderAuthContext containing the prompter, runtime, openUrl, and oauth.createVpsAwareHandlers utility helpers.configPatch when you need to add default model or provider configurations.defaultModel so that --set-default can update the Agent's default values.-
Registering Message Channels
Plugins can register channel plugins, which behave similarly to built-in channels (WhatsApp, Telegram, etc.). Channel configurations are located under channels.<id> and are 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 });}
Important Notes:
channels.<id> (rather than plugins.entries).meta.label is used for the labels displayed in CLI/UI lists.meta.aliases adds alternative IDs used for normalization and CLI input.meta.preferOver lists the IDs of channels to be skipped for auto-activation when both are configured.meta.detailLabel and meta.systemImage allow the UI to display richer channel labels and icons.Use this method when you want to create a new chat interface ("messaging channel") rather than a model provider. Documentation for model providers can be found under /providers/*.
channels.<id>.channels.<id>.accounts.<accountId>.meta.label, meta.selectionLabel, meta.docsPath, and meta.blurb control how the channel appears in CLI/UI lists.meta.docsPath should point to a documentation page, such as /channels/<id>.meta.preferOver allows your plugin to supersede another channel (ensuring it is automatically selected first).meta.detailLabel and meta.systemImage are used by the UI for displaying detailed text and icons.config.listAccountIds + config.resolveAccountcapabilities (chat types, media support, threading, etc.)outbound.deliveryMode + outbound.sendText (for basic message sending)setup (setup wizards), security (private messaging policies), status (health/diagnostics)gateway (start/stop/login), mentions, threading, streamingactions (message actions), commands (native command behaviors)api.registerChannel({ plugin })Minimal Configuration Example:
xxxxxxxxxx{channels: {acmechat: {accounts: {default: { token: "ACME_TOKEN", enabled: true },},},},}
Minimum Channels Plugin (Outbound Only):
xxxxxxxxxxconst plugin = {id: "acmechat",meta: {id: "acmechat",label: "AcmeChat",selectionLabel: "AcmeChat (API)",docsPath: "/channels/acmechat",blurb: "AcmeChat messaging channel.",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 ({ text }) => {// deliver `text` to your channel herereturn { ok: true };},},};export default function (api) {api.registerChannel({ plugin });}
Load the plugin (from the extensions directory or plugins.load.paths), restart the Gateway, and then configure channels.<id> within the configuration settings.
Refer to the dedicated guide: Plugin Agent Tools.
xxxxxxxxxxexport default function (api) {api.registerGatewayMethod("myplugin.status", ({ respond }) => {respond(true, { ok: true });});}
Register CLI Commands
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 invoking an AI agent. This is useful for toggle commands, status checks, or quick actions that do not require 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: The sender's ID (if available)channel: The channel where the command was sentisAuthorizedSender: Whether the sender is an authorized userargs: Arguments passed after the command (if acceptsArgs: true)commandBody: The full text of the commandconfig: The current OpenClaw configurationCommand Options:
name: The command name (without the leading /)description: The help text displayed in the command listacceptsArgs: Whether the command accepts arguments (Default: false). If false and arguments are provided, the command will not match, and the message will be passed to other handlers.requireAuth: Whether the sender must be authorized (Default: true)handler: A function that returns { text: string } (can be asynchronous)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}` };},});
Important Notes:
/MyStatus matches /mystatus).help, status, reset, etc.) cannot be overridden by plugins.xxxxxxxxxxexport 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)kebab-case or camelCase, but avoid conflicts with core commands.Plugins may ship with associated Skills located within their repository (skills/<name>/SKILL.md). Enable them using plugins.entries.<id>.enabled (or other configuration toggles), and ensure that the Skill file is present in your workspace or designated Skills directory.
Recommended Packaging Strategy:
openclaw (this repository)@openclaw/* scope (e.g., @openclaw/voice-call)Publishing Contract:
package.json must include an openclaw.extensions field specifying one or more entry files..js or .ts (jiti is used to load TypeScript files at runtime).openclaw plugins install <npm-spec> command executes npm pack, extracts the package contents to ~/.openclaw/extensions/<id>/, and automatically enables the plugin within the configuration.plugins.entries.* configuration keys. ## Example Plugin: Voice CallThis repository contains a voice call plugin (supporting Twilio or a log-based fallback):
extensions/voice-callskills/voice-callopenclaw voicecall start|statusvoice_callvoicecall.start, voicecall.statusprovider: "twilio" + twilio.accountSid/authToken/from (optional: statusCallbackUrl, twimlUrl)provider: "log" (offline mode)Refer to Voice Call and extensions/voice-call/README.md for setup and usage instructions.
Plugins run within the same process as the Gateway. Treat them as trusted code:
plugins.allow allowlist.Plugins can (and should) be accompanied by tests:
src/** (e.g., src/plugins/voice-call.plugin.test.ts).openclaw.extensions points to the built entry point (dist/index.js).
For more information regarding plugins, please refer to: Tools and Plugins - OpenClaw