Introduction
OpenClaw's architecture follows the philosophy of "everything is a plugin." Support for Telegram, Discord, Slack, and other chat platforms is all implemented through Channel plugins. When you need to connect a chat platform that OpenClaw doesn't yet support, or create a completely custom messaging channel, you need to develop your own Channel plugin.
This article walks you through the entire process from development to publishing for a Channel plugin, starting from scratch.
Plugin Architecture Overview
Channel Plugin Responsibilities
A Channel plugin is responsible for the following:
External Platform ←→ Channel Plugin ←→ OpenClaw Core ←→ AI Agent
│
├── Receive external messages and convert to OpenClaw internal format
├── Convert AI replies to the external platform's message format
├── Manage connections to the external platform (WebSocket / HTTP / polling)
└── Handle platform-specific features (buttons, cards, media, etc.)
Plugin Directory Structure
my-channel-plugin/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Plugin entry point
│ ├── channel.ts # Channel implementation
│ ├── message.ts # Message conversion
│ ├── connection.ts # Connection management
│ └── types.ts # Type definitions
├── test/
│ ├── channel.test.ts
│ └── message.test.ts
└── README.md
Setting Up the Development Environment
Initialize the Project
mkdir openclaw-channel-myplatform
cd openclaw-channel-myplatform
npm init -y
npm install @openclaw/plugin-sdk typescript
npm install -D @types/node vitest
Configure TypeScript
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
package.json Plugin Metadata
{
"name": "openclaw-channel-myplatform",
"version": "1.0.0",
"description": "OpenClaw Channel plugin for MyPlatform",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"openclaw": {
"type": "channel",
"name": "myplatform",
"displayName": "MyPlatform",
"version": "1.0.0",
"minGatewayVersion": "0.8.0"
}
}
Implementing the Channel Interface
Core Interface Definition
The OpenClaw Plugin SDK defines the interface that Channel plugins must implement:
// src/types.ts
import {
ChannelPlugin,
ChannelConfig,
IncomingMessage,
OutgoingMessage,
ChannelConnection,
ChannelCapabilities
} from "@openclaw/plugin-sdk";
export interface MyPlatformConfig extends ChannelConfig {
apiToken: string;
apiEndpoint?: string;
webhookPort?: number;
}
Implementing the Plugin Entry Point
// src/index.ts
import { PluginDefinition } from "@openclaw/plugin-sdk";
import { MyPlatformChannel } from "./channel";
const plugin: PluginDefinition = {
name: "myplatform",
displayName: "MyPlatform",
type: "channel",
// Config schema declaration (used for Dashboard UI generation)
configSchema: {
apiToken: {
type: "string",
required: true,
secret: true,
label: "API Token",
description: "Bot API Token for MyPlatform"
},
apiEndpoint: {
type: "string",
required: false,
default: "https://api.myplatform.com",
label: "API Endpoint"
}
},
// Create Channel instance
createChannel(config: MyPlatformConfig) {
return new MyPlatformChannel(config);
}
};
export default plugin;
Implementing the Channel Class
// src/channel.ts
import {
ChannelPlugin,
IncomingMessage,
OutgoingMessage,
ChannelCapabilities,
MessageHandler
} from "@openclaw/plugin-sdk";
import { MyPlatformConfig } from "./types";
import { MyPlatformConnection } from "./connection";
import { convertIncoming, convertOutgoing } from "./message";
export class MyPlatformChannel implements ChannelPlugin {
private config: MyPlatformConfig;
private connection: MyPlatformConnection;
private messageHandler: MessageHandler | null = null;
constructor(config: MyPlatformConfig) {
this.config = config;
this.connection = new MyPlatformConnection(config);
}
// Declare plugin capabilities
getCapabilities(): ChannelCapabilities {
return {
supportsDM: true,
supportsGroup: true,
supportsMedia: true,
supportsButtons: true,
supportsEditing: true,
supportsReactions: false,
supportsThreads: false,
supportsVoice: false,
maxMessageLength: 4096,
supportedMediaTypes: ["image", "file", "video"]
};
}
// Start connection
async start(handler: MessageHandler): Promise<void> {
this.messageHandler = handler;
// Connect to the external platform
await this.connection.connect();
// Listen for incoming messages
this.connection.on("message", async (rawMessage) => {
const message = convertIncoming(rawMessage);
if (this.messageHandler) {
await this.messageHandler(message);
}
});
console.log("[MyPlatform] Channel started successfully");
}
// Send message
async sendMessage(message: OutgoingMessage): Promise<string> {
const platformMessage = convertOutgoing(message);
const result = await this.connection.send(platformMessage);
return result.messageId; // Return platform message ID
}
// Edit a sent message
async editMessage(messageId: string, message: OutgoingMessage): Promise<void> {
const platformMessage = convertOutgoing(message);
await this.connection.edit(messageId, platformMessage);
}
// Stop connection
async stop(): Promise<void> {
await this.connection.disconnect();
console.log("[MyPlatform] Channel stopped");
}
// Health check
async healthCheck(): Promise<boolean> {
return this.connection.isConnected();
}
}
Implementing Message Conversion
// src/message.ts
import { IncomingMessage, OutgoingMessage } from "@openclaw/plugin-sdk";
// Convert platform message to OpenClaw internal format
export function convertIncoming(raw: any): IncomingMessage {
return {
id: raw.id,
userId: raw.sender.id,
userName: raw.sender.name,
channelId: raw.chat.id,
channelType: raw.chat.type === "private" ? "dm" : "group",
content: raw.text || "",
// Media attachments
attachments: (raw.attachments || []).map((a: any) => ({
type: a.type,
url: a.url,
filename: a.name,
mimeType: a.mimeType,
size: a.size
})),
// Raw data (for advanced use)
rawData: raw,
timestamp: new Date(raw.timestamp)
};
}
// Convert OpenClaw reply to platform message format
export function convertOutgoing(message: OutgoingMessage): any {
const result: any = {
chat_id: message.channelId,
text: message.content
};
// Handle rich text formatting
if (message.format === "markdown") {
result.parse_mode = "Markdown";
}
// Handle media
if (message.attachments?.length) {
result.attachments = message.attachments.map(a => ({
type: a.type,
url: a.url,
caption: a.caption
}));
}
// Handle buttons
if (message.buttons?.length) {
result.keyboard = message.buttons.map(b => ({
text: b.label,
callback_data: b.action
}));
}
return result;
}
Connection Management
// src/connection.ts
import { EventEmitter } from "events";
import { MyPlatformConfig } from "./types";
export class MyPlatformConnection extends EventEmitter {
private config: MyPlatformConfig;
private ws: WebSocket | null = null;
private connected = false;
constructor(config: MyPlatformConfig) {
super();
this.config = config;
}
async connect(): Promise<void> {
const endpoint = this.config.apiEndpoint || "https://api.myplatform.com";
// WebSocket connection example
this.ws = new WebSocket(`${endpoint}/ws?token=${this.config.apiToken}`);
this.ws.onopen = () => {
this.connected = true;
console.log("[MyPlatform] WebSocket connected");
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "message") {
this.emit("message", data.payload);
}
};
this.ws.onclose = () => {
this.connected = false;
// Auto-reconnect
setTimeout(() => this.connect(), 5000);
};
}
async send(message: any): Promise<{ messageId: string }> {
const response = await fetch(
`${this.config.apiEndpoint}/api/messages`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${this.config.apiToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(message)
}
);
return response.json();
}
async edit(messageId: string, message: any): Promise<void> {
await fetch(
`${this.config.apiEndpoint}/api/messages/${messageId}`,
{
method: "PUT",
headers: {
"Authorization": `Bearer ${this.config.apiToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(message)
}
);
}
async disconnect(): Promise<void> {
this.ws?.close();
this.connected = false;
}
isConnected(): boolean {
return this.connected;
}
}
Testing
Unit Tests
// test/message.test.ts
import { describe, it, expect } from "vitest";
import { convertIncoming, convertOutgoing } from "../src/message";
describe("Message Conversion", () => {
it("should convert incoming DM message", () => {
const raw = {
id: "msg_001",
sender: { id: "user_1", name: "John" },
chat: { id: "chat_1", type: "private" },
text: "Hello",
timestamp: "2026-03-14T10:00:00Z"
};
const result = convertIncoming(raw);
expect(result.userId).toBe("user_1");
expect(result.channelType).toBe("dm");
expect(result.content).toBe("Hello");
});
it("should convert outgoing message with buttons", () => {
const message = {
channelId: "chat_1",
content: "Please choose:",
buttons: [
{ label: "Option A", action: "option_a" },
{ label: "Option B", action: "option_b" }
]
};
const result = convertOutgoing(message);
expect(result.keyboard).toHaveLength(2);
expect(result.keyboard[0].text).toBe("Option A");
});
});
Integration Tests
# Use OpenClaw's plugin testing framework
openclaw plugin test ./openclaw-channel-myplatform \
--mock-platform \
--scenario send-receive
Local Debugging
Loading a Local Plugin in OpenClaw
{
plugins: {
local: [
// Load plugin from local path
"./plugins/openclaw-channel-myplatform"
]
},
channels: {
myplatform: {
enabled: true,
apiToken: "your-test-token"
}
}
}
# Start in development mode with hot reload support
openclaw start --dev --watch-plugins
Publishing the Plugin
Publish to npm
# Build
npm run build
# Publish
npm publish --access public
Submit to the OpenClaw Plugin Marketplace
- Create a repository on GitHub, ensuring it includes a complete README and LICENSE
- Submit a PR to the OpenClaw Plugin Marketplace with plugin metadata
- After passing review, users can install via:
openclaw plugin install openclaw-channel-myplatform
Installing Third-Party Plugins
# Install from npm
openclaw plugin install openclaw-channel-myplatform
# Install from GitHub
openclaw plugin install github:username/openclaw-channel-myplatform
# List installed plugins
openclaw plugin list
Lifecycle Hooks
Channel plugins can also implement the following optional lifecycle hooks:
export class MyPlatformChannel implements ChannelPlugin {
// Called when the plugin is loaded (initialize resources)
async onLoad(): Promise<void> { }
// Called when the plugin is unloaded (clean up resources)
async onUnload(): Promise<void> { }
// Called when configuration is updated (hot-reload config)
async onConfigUpdate(newConfig: MyPlatformConfig): Promise<void> { }
// Called when an Agent comes online
async onAgentOnline(agentId: string): Promise<void> { }
// Called when an Agent goes offline
async onAgentOffline(agentId: string): Promise<void> { }
}
Conclusion
OpenClaw's plugin architecture lets you connect any communication platform to the AI Agent ecosystem. By implementing a few core methods of the ChannelPlugin interface — start, sendMessage, stop — you have a working Channel plugin. The message conversion layer translates between platform formats and OpenClaw's internal format, while the connection management layer maintains communication with the external platform. Combined with a robust testing framework, local debugging support, and a plugin marketplace publishing workflow, OpenClaw makes plugin development efficient and organized.