Extending OpenClaw with Plugins
I have often found myself using a tool that does 90% of what I want, but that last 10% is missing. It is frustrating when you need a specific integration—like a certain chat platform or a specialized tool—and the core software just does not have it. You do not want to bloat the main installation for everyone, but you need that specific functionality to get your work done.
OpenClaw handles this through plugins. These are small code modules that extend the system with extra features like commands, tools, and Gateway RPC. You can use them to add features not yet built into the core or to keep your main install light.
What You’ll Need
Section titled “What You’ll Need”- OpenClaw Gateway
- TypeScript (Plugins are loaded as TypeScript modules via jiti)
Quick Start
Section titled “Quick Start”If you are new to plugins, here is the fastest way to get them running in 5 minutes.
-
Check your current setup: See what is already loaded on your system:
Terminal window openclaw plugins list -
Install a plugin: You can install official plugins directly. For example, to add Voice Call support:
Terminal window openclaw plugins install @openclaw/voice-call -
Restart and Configure: Restart your Gateway. You can then configure the plugin settings under
plugins.entries.<id>.configin your configuration file.
How Plugins Work
Section titled “How Plugins Work”Plugins run in-process with the Gateway, so you should only use code you trust. They are quite flexible and can register several types of features:
- Gateway RPC methods and HTTP handlers
- Agent tools and CLI commands
- Background services and config validation
- Skills and auto-reply commands
When you use a plugin for telephony, you can use built-in runtime helpers. For example, to convert text to speech:
const result = await api.runtime.tts.textToSpeechTelephony({ text: "Hello from OpenClaw", cfg: api.config,});This helper uses your core messages.tts configuration (OpenAI or ElevenLabs). It returns a PCM audio buffer and a sample rate, though you will need to handle any resampling or encoding required by your specific provider.
Troubleshooting
Section titled “Troubleshooting”- Validation issues: Keep in mind that config validation does not execute your plugin code. It uses the plugin manifest and JSON Schema instead. If your code has errors, they might not show up during the validation phase.
- Audio errors in telephony: Edge TTS is not supported for telephony. If you are building a plugin for voice, ensure you are using a supported provider.
If you run into other issues, you can ask the AI Setup Assistant for help.
What’s Next
Section titled “What’s Next”I have spent too many hours debugging why a specific script or extension isn’t loading, only to realize I put it in the wrong folder or a different version was overriding it. It is frustrating when you think you have everything set up, but the system just ignores your code.
I want to show you exactly how OpenClaw scans for plugins so you can avoid that headache. Understanding the discovery order is the best way to make sure your workspace and global extensions work exactly as you expect.
What You’ll Need
Section titled “What You’ll Need”- A plugin directory or file.
- An
openclaw.plugin.jsonmanifest file in your plugin’s root. - (Optional) A
package.jsonif you are grouping multiple extensions into a pack.
Quick Start
Section titled “Quick Start”If you want to get a plugin running immediately, the fastest path is using your global extensions folder.
- Create a TypeScript file at
~/.openclaw/extensions/my-plugin.ts. - Ensure there is an
openclaw.plugin.jsonfile in that same directory. - OpenClaw will pick it up automatically during the next scan.
If you are trying to use a bundled plugin that comes with OpenClaw, remember they are disabled by default. You need to enable them manually:
openclaw plugins enable <id>Discovery & Precedence
Section titled “Discovery & Precedence”I find it helpful to think of discovery as a top-down search. OpenClaw scans these locations in a specific order. If two plugins have the same ID, the one found first wins, and the others are ignored.
- Config paths: Any file or directory defined in
plugins.load.paths. - Workspace extensions:
<workspace>/.openclaw/extensions/*.ts<workspace>/.openclaw/extensions/*/index.ts
- Global extensions:
~/.openclaw/extensions/*.ts~/.openclaw/extensions/*/index.ts
- Bundled extensions: These are shipped with OpenClaw at
<openclaw>/extensions/*.
Installed plugins are enabled by default. However, bundled plugins must be enabled explicitly via plugins.entries.<id>.enabled in your config or by using the CLI.
Working with Package Packs
Section titled “Working with Package Packs”If you have a project with multiple extensions, you can group them using a package.json. I recommend this for keeping related tools together. You just need to add an openclaw.extensions array.
{ "name": "my-pack", "openclaw": { "extensions": ["./src/safety.ts", "./src/tools.ts"] }}When you do this, the plugin ID is automatically generated as name/<fileBase>. If your plugin needs npm dependencies, make sure you run npm install or pnpm install inside that directory so the node_modules are available.
Channel Catalog Metadata
Section titled “Channel Catalog Metadata”For those of you building channel plugins, you can include onboarding and installation hints directly in your package.json. This keeps the core OpenClaw catalog clean.
Here is an example of how to structure that metadata:
{ "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" } }}External Catalogs
Section titled “External Catalogs”You can also merge external channel catalogs, like an MPM registry export. You can drop a JSON file into one of these locations:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.json
If you prefer environment variables, point OPENCLAW_PLUGIN_CATALOG_PATHS or OPENCLAW_MPM_CATALOG_PATHS at your JSON files. Use commas, semicolons, or standard path delimiters to separate multiple files. Each file should follow this structure:
{ "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": { "...": "..." }, "install": { "...": "..." } } } ]}Troubleshooting
Section titled “Troubleshooting”Plugin is not appearing in the list
Check if the openclaw.plugin.json file exists in the root of the plugin. If you pointed OpenClaw at a specific file, the manifest must be in the same directory as that file.
Bundled plugin isn’t working
Bundled plugins are disabled by default. You must enable them using openclaw plugins enable <id>.
The wrong version of a plugin is loading OpenClaw uses a strict precedence order. A plugin in your workspace folder will always override a global plugin or a bundled one if they share the same ID.
If you have more questions about your specific setup, check out the AI Setup Assistant.
What’s Next
Section titled “What’s Next”Setting up a new tool often involves a mess of configuration files and conflicting extensions. I’ve been there—trying to figure out why a plugin won’t load or why two features are fighting over the same resource. It’s frustrating when you just want things to work without guessing which ID belongs to which file.
I want to show you how OpenClaw handles plugins so you can spend less time debugging and more time building. The system is strict about validation, which is great because it catches mistakes before they break your gateway.
What You’ll Need
Section titled “What You’ll Need”- An active OpenClaw installation.
- A plugin file (
.ts,.tgz,.zip) or an npm package. - Access to your JSON5 configuration file.
Quick Start
Section titled “Quick Start”You can get a plugin running in about five minutes. Here is the fastest path:
- Install the plugin: Use the CLI to add a local directory.
Terminal window openclaw plugins install ./extensions/voice-call - Enable it in config: Add the plugin ID to your
plugins.entries.{plugins: {entries: {"voice-call": { enabled: true, config: { provider: "twilio" } },},},} - Restart the gateway: Config changes are not hot-reloaded.
- Verify: Run
openclaw plugins listto see the status.
Plugin IDs
Section titled “Plugin IDs”Every plugin needs a unique ID. If you are using a package, I look at the name field in package.json. If it is a standalone file like ~/.../voice-call.ts, the ID becomes voice-call.
If your plugin code exports a specific id, I will use that. However, I will give you a warning if that exported ID doesn’t match what you’ve put in your configuration.
Configuration
Section titled “Configuration”The plugins block in your config is where you control everything. Here is a look at the available fields:
{ plugins: { enabled: true, allow: ["voice-call"], deny: ["untrusted-plugin"], load: { paths: ["~/Projects/oss/voice-call-extension"] }, entries: { "voice-call": { enabled: true, config: { provider: "twilio" } }, }, },}enabled: This is the master toggle. It defaults totrue.allow/deny: These are your allowlists and denylists. If a plugin is in both,denywins.load.paths: Point this to extra directories where you keep plugin files.entries.<id>: This is where you put per-plugin settings and toggles.
Remember that config changes require a gateway restart. I also validate your config strictly. If you use an unknown ID in entries, allow, deny, or slots, I will throw an error.
Plugin Slots
Section titled “Plugin Slots”Some plugins belong to exclusive categories where only one can be active at a time. I use plugins.slots to handle this. For example, if you have multiple memory plugins, you have to pick one:
{ plugins: { slots: { memory: "memory-core", // or "none" to disable memory plugins }, },}If multiple plugins try to claim the memory kind, only the one you selected in slots will load. I will disable the others and provide diagnostics so you know why they aren’t running.
Control UI
Section titled “Control UI”The Control UI makes things easier by rendering forms for your plugin settings. I use configSchema and uiHints from your plugin manifest to build these.
If you want your plugin to look good in the UI, provide uiHints like this:
{ "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" } }}I’ll automatically add labels for the enabled and config fields at runtime.
CLI Commands
Section titled “CLI Commands”The CLI is the best way to manage your extensions. Here are the commands you’ll use most:
openclaw 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 -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 doctorNote that plugins update only works for npm installs that I track under plugins.installs.
Plugin API and Hooks
Section titled “Plugin API and Hooks”Plugins can be a simple function or an object. If you want to bundle automation, you can ship hooks directly inside your plugin.
import { registerPluginHooksFromDir } from "openclaw/plugin-sdk";
export default function register(api) { registerPluginHooksFromDir(api, "./hooks");}These hooks will show up in openclaw hooks list as plugin:<id>. You can’t disable these specific hooks individually; you have to disable the entire plugin.
Troubleshooting
Section titled “Troubleshooting”If things aren’t working, check these common issues:
- Validation Errors: If you have unknown plugin IDs in
entriesorallow, I will trigger an error. - Channel Errors: Unknown
channels.<id>keys are errors unless a plugin manifest specifically declares that channel ID. - Config Warnings: If you disable a plugin but leave its configuration in the file, I will preserve the settings but emit a warning.
- Schema Mismatch: Plugin configuration is validated against the JSON Schema in
openclaw.plugin.json. Ensure your config matches theconfigSchema.
If you’re still stuck, try running openclaw plugins doctor to find hidden issues.
Still have questions? Ask the AI Setup Assistant.
What’s Next
Section titled “What’s Next”- Plugin Config Schema (openclaw.plugin.json)
- Managing Installs (plugins.installs)
- Working with Hooks (openclaw hooks)
- CLI Reference
I know the feeling of getting stuck in “integration hell.” You want to try a new model or connect a new chat app, but you end up writing the same OAuth boilerplate or API key handlers over and over. It’s a distraction from actually building your agent.
I want to show you how OpenClaw solves this with its plugin system. Instead of external scripts, you can bake model auth and messaging channels directly into the platform. This makes setup a lot smoother for anyone using your plugin.
What You’ll Need
Section titled “What You’ll Need”- An OpenClaw plugin entry point (a function receiving the
apiobject) - Provider credentials (like OAuth client IDs or API keys)
- The OpenClaw CLI installed for testing auth flows
Quick Start: Model Provider Auth
Section titled “Quick Start: Model Provider Auth”The goal here is to let users run openclaw models auth login directly. You do this by registering a provider with api.registerProvider(...).
Here is the basic structure for an OAuth flow:
api.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", }; }, }, ],});The run function is where the magic happens. It gives you a ProviderAuthContext with helpers like prompter and openUrl. If you return defaultModel, users can use the --set-default flag to update their agent settings automatically.
Once registered, a user just runs:
openclaw models auth login --provider acme --method oauth
Adding a Messaging Channel
Section titled “Adding a Messaging Channel”If you want to add a new chat surface—like a custom internal tool or a niche messaging app—you use api.registerChannel. This makes your custom channel behave exactly like the built-in Telegram or WhatsApp integrations.
1. Define the Channel
Section titled “1. Define the Channel”You need to set up the metadata and the outbound adapters. Here is a minimal outbound-only example:
const 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 here return { ok: true }; }, },};
export default function (api) { api.registerChannel({ plugin });}2. Configure the Channel
Section titled “2. Configure the Channel”Users will need to add the configuration to their config file under the channels.<id> key:
{ channels: { acmechat: { accounts: { default: { token: "ACME_TOKEN", enabled: true }, }, }, },}Custom Commands and Services
Section titled “Custom Commands and Services”Sometimes you need a command that doesn’t need an LLM to think. For example, a /status command should just return data immediately.
I use api.registerCommand for this. These slash commands are processed before the AI agent even sees the message.
api.registerCommand({ name: "setmode", description: "Set plugin mode", acceptsArgs: true, requireAuth: true, handler: async (ctx) => { const mode = ctx.args?.trim() || "default"; // You can access ctx.senderId, ctx.channel, and ctx.config here return { text: `Mode set to: ${mode}` }; },});If you need something running in the background, use api.registerService:
api.registerService({ id: "my-service", start: () => api.logger.info("ready"), stop: () => api.logger.info("bye"),});Troubleshooting
Section titled “Troubleshooting”Duplicate Commands
Section titled “Duplicate Commands”If you try to register a command name that another plugin is already using, the registration will fail with a diagnostic error. Note that you cannot override reserved names like help, status, or reset.
Command Not Matching
Section titled “Command Not Matching”If you set acceptsArgs: false but a user provides arguments anyway, the command won’t match. The message will fall through to other handlers or the AI agent.
Auth Failures
Section titled “Auth Failures”Ensure your run function in registerProvider returns the correct credential shape. If the profileId doesn’t match the provider prefix, the CLI might have trouble mapping the credentials.
Still stuck? Check out the AI Setup Assistant for real-time help.
What’s Next
Section titled “What’s Next”- Plugin agent tools - Learn how to give your agent specific capabilities.
- Messaging Channels - Deep dive into channel-specific configurations.
I have spent far too many hours refactoring code because I didn’t stick to a clear naming convention from day one. It is a common headache: you start building, things get messy, and suddenly your CLI commands clash with your API methods. I want to help you avoid that mess by showing you exactly how we handle naming and distribution here.
Consistency makes your plugins easier to use and maintain. When everyone follows the same patterns, the whole system feels predictable.
What You’ll Need
Section titled “What You’ll Need”- Access to the
openclawrepository - npm for package distribution
Quick Start
Section titled “Quick Start”Getting a plugin up and running requires following a specific naming structure and installation path. Here is the 5-minute path to getting it right.
1. Follow the Naming Conventions
Section titled “1. Follow the Naming Conventions”I recommend sticking strictly to these patterns to avoid conflicts:
- Gateway methods: Use
pluginId.action(for example:voicecall.status). - Tools: Use
snake_case(for example:voice_call). - CLI commands: Use kebab-case or camelCase, but make sure they do not clash with core commands.
2. Set Up Your Distribution
Section titled “2. Set Up Your Distribution”We use npm for packaging. I suggest keeping the main package as openclaw and putting plugins under the @openclaw/* scope (like @openclaw/voice-call).
Your package.json must include the openclaw.extensions key with your entry files:
{ "name": "@openclaw/voice-call", "openclaw": { "extensions": ["./dist/index.js"] }}Entry files can be .js or .ts. If you use TypeScript, jiti handles the runtime loading for you.
3. Install and Enable
Section titled “3. Install and Enable”Use the CLI to pull in your plugin:
openclaw plugins install <npm-spec>This command uses npm pack, extracts the files into ~/.openclaw/extensions/<id>/, and enables it in your config. Note that scoped packages are normalized to their unscoped id for the plugins.entries.* configuration key.
4. Example: Voice Call Plugin
Section titled “4. Example: Voice Call Plugin”If you want to see this in action, check the voice-call plugin in the repo:
- Source:
extensions/voice-call - CLI:
openclaw voicecall start|status - Tool:
voice_call - RPC:
voicecall.start,voicecall.status
You can configure it for Twilio or a simple log fallback:
# Twilio configprovider: "twilio"twilio: accountSid: "your_sid" authToken: "your_token" from: "+123456789" statusCallbackUrl: "http://example.com/callback" twimlUrl: "http://example.com/twiml"
# Dev configprovider: "log"Troubleshooting
Section titled “Troubleshooting”Config Key Mismatches
Section titled “Config Key Mismatches”If your configuration isn’t loading, check your plugin ID normalization. If you are using a scoped package like @openclaw/voice-call, the config key in plugins.entries.* will be voice-call, not the full scoped name.
Changes Not Reflecting
Section titled “Changes Not Reflecting”Plugins run in-process with the Gateway. Because they are treated as trusted code, you must restart the Gateway after making any changes to the plugin code or configuration.
Security and Safety
Section titled “Security and Safety”If a plugin isn’t working as expected, verify your safety settings:
- Only install plugins from sources you trust.
- Use
plugins.allowallowlists to restrict what can run. - Ensure your
openclaw.extensionspoints at the correct built entrypoint (e.g.,dist/index.js). - Verify that in-repo plugins have their tests passing via Vitest.
Need more help getting your naming right? Chat with the AI Setup Assistant.
What’s Next
Section titled “What’s Next”OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.