Skip to content

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.

  • OpenClaw Gateway
  • TypeScript (Plugins are loaded as TypeScript modules via jiti)

If you are new to plugins, here is the fastest way to get them running in 5 minutes.

  1. Check your current setup: See what is already loaded on your system:

    Terminal window
    openclaw plugins list
  2. Install a plugin: You can install official plugins directly. For example, to add Voice Call support:

    Terminal window
    openclaw plugins install @openclaw/voice-call
  3. Restart and Configure: Restart your Gateway. You can then configure the plugin settings under plugins.entries.<id>.config in your configuration file.

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.

  • 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.

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.

  • A plugin directory or file.
  • An openclaw.plugin.json manifest file in your plugin’s root.
  • (Optional) A package.json if you are grouping multiple extensions into a pack.

If you want to get a plugin running immediately, the fastest path is using your global extensions folder.

  1. Create a TypeScript file at ~/.openclaw/extensions/my-plugin.ts.
  2. Ensure there is an openclaw.plugin.json file in that same directory.
  3. 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:

Terminal window
openclaw plugins enable <id>

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.

  1. Config paths: Any file or directory defined in plugins.load.paths.
  2. Workspace extensions:
    • <workspace>/.openclaw/extensions/*.ts
    • <workspace>/.openclaw/extensions/*/index.ts
  3. Global extensions:
    • ~/.openclaw/extensions/*.ts
    • ~/.openclaw/extensions/*/index.ts
  4. 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.

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.

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"
}
}
}

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": { "...": "..." }
}
}
]
}

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.

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.

  • An active OpenClaw installation.
  • A plugin file (.ts, .tgz, .zip) or an npm package.
  • Access to your JSON5 configuration file.

You can get a plugin running in about five minutes. Here is the fastest path:

  1. Install the plugin: Use the CLI to add a local directory.
    Terminal window
    openclaw plugins install ./extensions/voice-call
  2. Enable it in config: Add the plugin ID to your plugins.entries.
    {
    plugins: {
    entries: {
    "voice-call": { enabled: true, config: { provider: "twilio" } },
    },
    },
    }
  3. Restart the gateway: Config changes are not hot-reloaded.
  4. Verify: Run openclaw plugins list to see the status.

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.

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 to true.
  • allow / deny: These are your allowlists and denylists. If a plugin is in both, deny wins.
  • 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.

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.

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.

The CLI is the best way to manage your extensions. Here are the commands you’ll use most:

Terminal window
openclaw plugins list
openclaw plugins info <id>
openclaw plugins install <path> # copy a local file/dir into ~/.openclaw/extensions/<id>
openclaw plugins install ./extensions/voice-call # relative path ok
openclaw plugins install ./plugin.tgz # install from a local tarball
openclaw plugins install -l ./extensions/voice-call # link (no copy) for dev
openclaw plugins install @openclaw/voice-call # install from npm
openclaw plugins update <id>
openclaw plugins update --all
openclaw plugins enable <id>
openclaw plugins disable <id>
openclaw plugins doctor

Note that plugins update only works for npm installs that I track under plugins.installs.

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.

If things aren’t working, check these common issues:

  • Validation Errors: If you have unknown plugin IDs in entries or allow, 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 the configSchema.

If you’re still stuck, try running openclaw plugins doctor to find hidden issues.

Still have questions? Ask the AI Setup Assistant.

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.

  • An OpenClaw plugin entry point (a function receiving the api object)
  • Provider credentials (like OAuth client IDs or API keys)
  • The OpenClaw CLI installed for testing auth flows

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

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.

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 });
}

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 },
},
},
},
}

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"),
});

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.

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.

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.

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.

  • Access to the openclaw repository
  • npm for package distribution

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.

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.

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.

Use the CLI to pull in your plugin:

Terminal window
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.

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 config
provider: "twilio"
twilio:
accountSid: "your_sid"
authToken: "your_token"
from: "+123456789"
statusCallbackUrl: "http://example.com/callback"
twimlUrl: "http://example.com/twiml"
# Dev config
provider: "log"

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.

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.

If a plugin isn’t working as expected, verify your safety settings:

  1. Only install plugins from sources you trust.
  2. Use plugins.allow allowlists to restrict what can run.
  3. Ensure your openclaw.extensions points at the correct built entrypoint (e.g., dist/index.js).
  4. Verify that in-repo plugins have their tests passing via Vitest.

Need more help getting your naming right? Chat with the AI Setup Assistant.

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.