January 31, 2026
OpenClaw is an open source AI agent gateway — it connects LLMs like Claude to messaging platforms (WhatsApp, Telegram, Discord, iMessage) and gives them tools to actually do things: run commands, browse the web, read files, query databases.
But the real power is that it’s extensible. If the built-in channels don’t cover your use case, you can build your own plugin. That’s what this guide is about.
We’ll walk through creating a channel plugin from scratch. By the end, you’ll have a working plugin installed in your OpenClaw gateway. What you do with it from there is up to you.
The OpenClaw plugin system supports several types of extensions:
openclaw CLIThis guide focuses on channel plugins — the most common type. A channel plugin gives your agent a new way to talk to the world.
The plugin ecosystem is wide open. A few ideas to get your gears turning:
Our dashbot-openclaw plugin is one example — it connects an AI agent to a Rails dashboard over WebSockets. But your plugin can be as simple or complex as you need.
Here’s how a plugin fits into the OpenClaw ecosystem:
graph TD
App["Your App / Service"]
Plugin["Your Plugin"]
Gateway["OpenClaw Gateway"]
Agent["AI Agent"]
App <-->|any protocol| Plugin
Plugin <-->|Plugin API| Gateway
Gateway <--> Agent
The plugin runs inside the gateway process. It uses the OpenClaw Plugin API to register itself, receive messages from the agent, and send messages back. How your plugin communicates with the outside world — HTTP, WebSocket, carrier pigeon — is entirely up to you.
Create a new directory and initialize the project:
mkdir my-openclaw-plugin
cd my-openclaw-plugin
npm init -y
Install TypeScript and the test runner:
npm install -D typescript vitest @types/node
Generate a TypeScript config:
npx tsc --init
Then update tsconfig.json to use modern ES modules:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
},
"include": ["src/**/*"]
}
Update your package.json to set the module type and scripts:
{
"type": "module",
"main": "./dist/index.js",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run"
}
}
Every OpenClaw plugin needs an openclaw.plugin.json in its root directory. This tells the gateway what your plugin is and how to load it. See the plugin manifest docs for the full spec.
Create openclaw.plugin.json:
{
"id": "my-plugin",
"name": "My OpenClaw Plugin",
"version": "1.0.0",
"description": "A custom channel plugin",
"author": "Your Name",
"license": "MIT"
}
You’ll also need to tell OpenClaw where to find your entry point. Add this to your package.json:
{
"openclaw": {
"extensions": ["./src/index.ts"]
}
}
OpenClaw loads TypeScript files directly at runtime via jiti, so you don’t even need to compile during development. The dist/ build is only needed if you publish to npm.
Every plugin exports a single register function. This is the entire contract between your code and OpenClaw.
Create src/index.ts:
export default function register(api: any) {
api.logger.info("Hello from my plugin!");
api.registerChannel({ plugin: myChannel });
}
When the gateway starts, it discovers your plugin, calls register, and passes in the Plugin API object. That API gives you everything you need: logging, config access, channel registration, and more.
A channel plugin is an object that describes how your plugin communicates. Here’s the minimal structure — see the channel plugin guide for all the options:
const myChannel = {
id: "my-channel",
meta: {
id: "my-channel",
label: "My Channel",
selectionLabel: "My Channel (custom)",
docsPath: "/channels/my-channel",
blurb: "A custom channel plugin for OpenClaw.",
aliases: ["mine"],
},
capabilities: {
chatTypes: ["direct"],
},
config: {
listAccountIds: (cfg: any) =>
Object.keys(cfg.channels?.["my-channel"]?.accounts ?? {}),
resolveAccount: (cfg: any, accountId?: string) =>
cfg.channels?.["my-channel"]?.accounts?.[accountId ?? "default"] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct" as const,
sendText: async ({ text }: { text: string }) => {
// This is called when the agent sends a message
// through your channel. Do whatever you want here:
// POST to an API, write to a file, flash a light...
console.log(`Agent says: ${text}`);
return { ok: true };
},
},
};
The key pieces:
meta — How your channel appears in the CLI and UI (docs)capabilities — What your channel supports (DMs, groups, media, threads)config — How OpenClaw resolves accounts from your channel configoutbound.sendText — The function called when the agent needs to send a message through your channelThat sendText function is where the magic happens. For a hello-world plugin, logging to the console is fine. For something like DashBot, we send messages over WebSocket to a Rails app. For a Slack integration, you’d call the Slack API. It’s your code — do what you want.
Here’s the complete src/index.ts:
const myChannel = {
id: "my-channel",
meta: {
id: "my-channel",
label: "My Channel",
selectionLabel: "My Channel (custom)",
docsPath: "/channels/my-channel",
blurb: "A custom channel plugin.",
aliases: ["mine"],
},
capabilities: {
chatTypes: ["direct"],
},
config: {
listAccountIds: (cfg: any) =>
Object.keys(cfg.channels?.["my-channel"]?.accounts ?? {}),
resolveAccount: (cfg: any, accountId?: string) =>
cfg.channels?.["my-channel"]?.accounts?.[accountId ?? "default"] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct" as const,
sendText: async ({ text }: { text: string }) => {
console.log(`[my-channel] Agent says: ${text}`);
return { ok: true };
},
},
};
export default function register(api: any) {
api.logger.info("My plugin loaded!");
api.registerChannel({ plugin: myChannel });
}
Around 40 lines of code. That’s a working channel plugin.
You should test your plugin — even the simple ones. Create src/index.test.ts:
import { describe, it, expect, vi } from "vitest";
import register from "./index.js";
describe("MyPlugin", () => {
it("registers a channel", () => {
const api = {
logger: { info: vi.fn() },
registerChannel: vi.fn(),
};
register(api);
expect(api.registerChannel).toHaveBeenCalledTimes(1);
expect(api.registerChannel).toHaveBeenCalledWith(
expect.objectContaining({
plugin: expect.objectContaining({ id: "my-channel" }),
})
);
});
});
Run it:
npm test
The OpenClaw plugin docs have more details on testing strategies, including how to validate your manifest and test against the real gateway.
Install your plugin into OpenClaw. For local development, use the -l flag to link (no copy):
openclaw plugins install -l ./path/to/my-openclaw-plugin
Or without the link flag to copy it into the extensions directory:
openclaw plugins install ./path/to/my-openclaw-plugin
See Plugin CLI docs for all install options, including installing from npm.
Now configure your channel. Add this to your OpenClaw config:
{
"channels": {
"my-channel": {
"accounts": {
"default": {
"enabled": true
}
}
}
}
}
Restart the gateway:
openclaw gateway restart
Check the logs:
openclaw logs
You should see your “My plugin loaded!” message. When the agent responds through your channel, you’ll see the [my-channel] Agent says: ... output.
This guide gets you a working plugin. Here’s where to go next:
A channel that only outputs isn’t very useful. To receive messages into OpenClaw, you’ll want to set up a gateway service that listens for incoming data (HTTP webhook, WebSocket, polling — whatever fits your use case) and dispatches messages into the agent session.
Need to expose custom API endpoints? Register gateway RPC methods that other tools and plugins can call.
Want a custom openclaw my-thing command? Register CLI commands directly from your plugin.
Give the agent new capabilities with custom tools — database queries, API calls, hardware control, anything you can code.
When your plugin is ready to share, publish it as an npm package. Others can install it with openclaw plugins install your-package-name.
We built DashBot as a real-time dashboard for our AI agent — chat, status monitoring, and interactive agentic cards, all in a browser.
The dashbot-openclaw plugin connects the gateway to DashBot over WebSockets (Action Cable). It handles bidirectional chat, streams agent status data, and relays card interactions. It’s a good example of what a more complex channel plugin looks like once you move past hello-world.
Both are MIT licensed and open source under wembledev on GitHub.
The plugin ecosystem is early. The best plugins haven’t been built yet. Maybe yours is one of them.
Questions? Ideas? Find us on GitHub or get in touch.