Skip to content

OpenClaw Plugins: Extend Your AI Agent

Want a feature that’s not in core OpenClaw? Plugins are the answer. They’re small code modules that add commands, tools, channels, or even full integration flows—without touching the main codebase.

I use plugins for everything from voice calls to custom Slack integrations. Once you get the pattern, they’re surprisingly easy to build.


  • OpenClaw installed and running
  • Basic TypeScript knowledge (for custom plugins)

Terminal window
openclaw plugins list
Terminal window
openclaw plugins install @openclaw/voice-call

Restart your Gateway, then add config under plugins.entries.<id>.config:

{
plugins: {
entries: {
"voice-call": {
enabled: true,
config: {
provider: "twilio"
}
}
}
}
}

Done. Your new plugin is ready.


PluginPackageDescription
Voice Call@openclaw/voice-callMake and receive phone calls
Microsoft Teams@openclaw/msteamsTeams channel integration
Matrix@openclaw/matrixMatrix chat protocol
Nostr@openclaw/nostrNostr decentralized chat
Zalo@openclaw/zaloVietnamese messaging app

Bundled plugins (disabled by default):

  • Memory (Core) — basic memory search
  • Memory (LanceDB) — long-term memory with auto-recall
  • Google/Gemini/Qwen OAuth — provider authentication flows

Enable bundled plugins with:

Terminal window
openclaw plugins enable memory-lancedb

OpenClaw scans for plugins in this order:

  1. Config paths — plugins.load.paths
  2. Workspace extensions — .openclaw/extensions/*.ts
  3. Global extensions — ~/.openclaw/extensions/*.ts
  4. Bundled — shipped with OpenClaw (disabled by default)

First match wins. Later copies are ignored.


{
plugins: {
enabled: true,
allow: ["voice-call"], // allowlist (optional)
deny: ["untrusted-plugin"], // denylist takes priority
load: {
paths: ["~/my-plugins/custom"]
},
entries: {
"voice-call": {
enabled: true,
config: { provider: "twilio" }
}
}
}
}

Config changes require a Gateway restart.


Some categories allow only one active plugin (like memory providers):

{
plugins: {
slots: {
memory: "memory-lancedb" // or "memory-core" or "none"
}
}
}

Terminal window
openclaw plugins list # See all plugins
openclaw plugins info <id> # Plugin details
openclaw plugins install &lt;path|npm&gt; # Install plugin
openclaw plugins install -l <path> # Link for development
openclaw plugins enable <id> # Enable plugin
openclaw plugins disable <id> # Disable plugin
openclaw plugins update <id> # Update npm plugin
openclaw plugins update --all # Update all npm plugins
openclaw plugins doctor # Diagnose issues

Create ~/.openclaw/extensions/my-plugin/index.ts:

export default function(api) {
api.registerGatewayMethod("myplugin.status", ({ respond }) => {
respond(true, { status: "running" });
});
}

Create ~/.openclaw/extensions/my-plugin/openclaw.plugin.json:

{
"id": "my-plugin",
"name": "My Custom Plugin",
"version": "1.0.0"
}

Restart Gateway—your plugin is live.

export default function(api) {
api.registerTool({
name: "my_tool",
description: "Does something useful",
parameters: {
type: "object",
properties: {
input: { type: "string" }
}
},
handler: async ({ input }) => {
return { result: `Processed: ${input}` };
}
});
}
export default function(api) {
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
handler: (ctx) => ({
text: `Plugin running on ${ctx.channel}`
})
});
}

This runs without invoking the AI—great for quick toggles and status checks.

const plugin = {
id: "acmechat",
meta: {
label: "AcmeChat",
docsPath: "/channels/acmechat",
blurb: "AcmeChat messaging."
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: (cfg) =>
Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, id) =>
cfg.channels?.acmechat?.accounts?.[id ?? "default"]
},
outbound: {
deliveryMode: "direct",
sendText: async ({ text }) => {
// Send message
return { ok: true };
}
}
};
export default function(api) {
api.registerChannel({ plugin });
}

Plugins can ship hooks and register them at runtime. This lets a plugin bundle event-driven automation without a separate hook pack install.

import { registerPluginHooksFromDir } from "openclaw/plugin-sdk";
export default function register(api) {
registerPluginHooksFromDir(api, "./hooks");
}

Notes:

  • Hook directories follow the normal hook structure (HOOK.md + handler.ts)
  • Hook eligibility rules still apply (OS/bins/env/config requirements)
  • Plugin-managed hooks show up in openclaw hooks list with plugin:<id>
  • Enable/disable the plugin to control its hooks

Plugins can access selected core helpers via api.runtime. For example, telephony TTS:

const result = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from OpenClaw",
cfg: api.config,
});

Notes:

  • Uses core messages.tts configuration (OpenAI or ElevenLabs)
  • Returns PCM audio buffer + sample rate
  • Plugins must resample/encode for their providers
  • Edge TTS is not supported for telephony

Plugins can register model provider auth flows so users can run OAuth or API-key setup inside OpenClaw.

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

Notes:

  • run receives a ProviderAuthContext with prompter, runtime, openUrl helpers
  • Return configPatch when you need to add default models
  • Return defaultModel so --set-default can update agent defaults

Users authenticate with:

Terminal window
openclaw models auth login --provider acme --method oauth

Plugins can register custom slash commands that execute without invoking the AI agent:

export default function(api) {
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
acceptsArgs: false,
requireAuth: true,
handler: (ctx) => ({
text: `Plugin running on ${ctx.channel}`
})
});
}
FieldDescription
senderIdSender’s ID
channelChannel where command was sent
isAuthorizedSenderWhether sender is authorized
argsArguments (if acceptsArgs: true)
commandBodyFull command text
configCurrent OpenClaw config
OptionDescription
nameCommand name (without /)
descriptionHelp text
acceptsArgsWhether to accept arguments (default: false)
requireAuthRequire authorized sender (default: true)
handlerFunction returning { text: string }

Notes:

  • Plugin commands process before built-in commands and the AI agent
  • Command names are case-insensitive
  • Reserved commands (help, status, reset) cannot be overridden

export default function(api) {
api.registerService({
id: "my-service",
start: () => api.logger.info("ready"),
stop: () => api.logger.info("bye"),
});
}

export default function(api) {
api.registerCli(({ program }) => {
program.command("mycmd").action(() => {
console.log("Hello");
});
}, { commands: ["mycmd"] });
}

Every plugin needs openclaw.plugin.json:

{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"configSchema": {
"type": "object",
"properties": {
"apiKey": { "type": "string" }
}
},
"uiHints": {
"apiKey": { "label": "API Key", "sensitive": true }
}
}

  1. Add to package.json:
{
"name": "@yourscope/my-plugin",
"openclaw": {
"extensions": ["./index.ts"]
}
}
  1. Publish: npm publish

  2. Users install with:

Terminal window
openclaw plugins install @yourscope/my-plugin

Check:

  1. Is plugins.enabled true?
  2. Is the plugin in deny list?
  3. Does openclaw.plugin.json exist?

Cause: Unknown plugin IDs in config are strict errors.

Fix: Remove references to disabled/uninstalled plugins from entries, allow, deny.

Cause: Multiple plugins with same ID.

Fix: Only first-discovered plugin loads. Remove duplicates from extension directories.

Still stuck? Our AI Setup Assistant can help debug your plugin setup.


A plugin directory may include a package.json with openclaw.extensions:

{
"name": "my-pack",
"openclaw": {
"extensions": ["./src/safety.ts", "./src/tools.ts"]
}
}

Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id becomes name/<fileBase>.

If your plugin imports npm deps, install them in that directory:

Terminal window
cd ~/.openclaw/extensions/my-pack
npm install

Channel plugins can advertise onboarding metadata via openclaw.channel and install hints via openclaw.install:

{
"name": "@openclaw/nextcloud-talk",
"openclaw": {
"extensions": ["./index.ts"],
"channel": {
"id": "nextcloud-talk",
"label": "Nextcloud Talk",
"selectionLabel": "Nextcloud Talk (self-hosted)",
"docsPath": "/channels/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: Drop JSON files at:

  • ~/.openclaw/mpm/plugins.json
  • ~/.openclaw/mpm/catalog.json
  • ~/.openclaw/plugins/catalog.json

Or set OPENCLAW_PLUGIN_CATALOG_PATHS environment variable.


Use this when you want a new chat surface (not a model provider).

All channel config lives under channels.<id>:

{
channels: {
acmechat: {
accounts: {
default: { token: "TOKEN", enabled: true }
}
}
}
}
FieldPurpose
meta.labelDisplay name in CLI/UI
meta.selectionLabelLonger selection text
meta.docsPathLink to docs (e.g., /channels/acmechat)
meta.blurbShort description
meta.aliasesAlternative channel IDs
meta.preferOverReplace another channel
const plugin = {
id: "acmechat",
meta: { /* ... */ },
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, id) => cfg.channels?.acmechat?.accounts?.[id ?? "default"]
},
outbound: {
deliveryMode: "direct",
sendText: async ({ text }) => ({ ok: true })
}
};
AdapterPurpose
setupWizard integration
securityDM policy
statusHealth/diagnostics
gatewayStart/stop/login
mentions@mention handling
threadingThread support
streamingStreaming responses
actionsMessage actions
commandsNative command behavior
export default function(api) {
api.registerChannel({ plugin });
}

TypeConventionExample
Gateway methodspluginId.actionvoicecall.status
Toolssnake_casevoice_call
CLI commandskebab-casevoicecall-start

Avoid clashing with core commands.


Plugins can ship skills by adding a skills/ directory:

my-plugin/
├── index.ts
├── openclaw.plugin.json
└── skills/
└── my-skill/
└── SKILL.md

Enable with plugins.entries.<id>.enabled and ensure it’s present in your managed skills locations.


Plugins run in-process with the Gateway. Treat them as trusted code:

  • Only install plugins you trust
  • Prefer plugins.allow allowlists
  • Restart the Gateway after changes
  • Review plugin source before enabling

Plugins should ship tests:

  • In-repo plugins: Keep Vitest tests under src/** (example: src/plugins/voice-call.plugin.test.ts)
  • Published plugins: Run own CI and validate openclaw.extensions points at built entrypoint
Terminal window
# Run plugin tests
cd ~/.openclaw/extensions/my-plugin
npm test


Need help? Join the OpenClaw Discord or check the GitHub Issues.

OpenClaw

OpenClaw Expert

Still stuck?

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