Home Tutorials Categories Skills About
ZH EN JA KO
Advanced

Complete Guide to OpenClaw Plugin Development: From Zero to Publishing

· 29 min read

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

  1. Create a repository on GitHub, ensuring it includes a complete README and LICENSE
  2. Submit a PR to the OpenClaw Plugin Marketplace with plugin metadata
  3. 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.

OpenClaw is a free, open-source personal AI assistant that supports WhatsApp, Telegram, Discord, and many more platforms