Channel Extensions

Focus Deep-Dive

How messaging channel plugins connect users to the AI agent

Channel extensions implement the ChannelPlugin interface — a comprehensive adapter-based contract with 15+ pluggable adapters covering messaging, outbound delivery, security, threading, groups, gateway HTTP methods, and agent integration. Each channel (Telegram, Discord, Slack, etc.) composes the adapters it needs.

📚 What is this & why does it matter?

The ChannelPlugin interface is how OpenClaw bridges 20+ messaging platforms. Instead of writing platform-specific code throughout the core, each channel implements a set of adapter interfaces. The core sees uniform “send message,” “receive message,” and “check auth” operations — the channel adapter translates these into platform-specific API calls.

💡 Real-world analogy

Channel extensions are like interpreters at a multilingual conference. The speaker (agent engine) delivers one message. Each interpreter (channel extension) listens through the same headset (ChannelPlugin interface), then translates and delivers the message in the local language (Telegram Bot API, Discord REST, Slack Web API). They also listen for questions from the audience (inbound messages) and translate them back into the speaker’s language (normalized inbound events).

Contents

Overview

Primary role

Bridge external messaging platforms to OpenClaw’s agent engine through a uniform adapter interface.

Main inputs

Inbound webhooks/polling from platforms, outbound messages from the agent, config with bot tokens.

Key decisions

Which adapters to implement, capability flags, security policy, threading model, gateway method registration.

Outputs / side effects

Delivered messages on platforms, webhook responses, account binding, group management, status probes.

flowchart LR
  subgraph Platforms["Messaging Platforms"]
    TG["Telegram"]
    DC["Discord"]
    SL["Slack"]
    WA["WhatsApp"]
    More["20+ more..."]
  end
  subgraph Channel["ChannelPlugin Adapters"]
    GW["Gateway Adapter"]
    Msg["Messaging Adapter"]
    Out["Outbound Adapter"]
    Sec["Security Adapter"]
    Thread["Threading Adapter"]
  end
  subgraph Core["OpenClaw Core"]
    Gateway["HTTP Gateway"]
    Engine["Agent Engine"]
    Sessions["Session Manager"]
  end
  TG --> GW
  DC --> GW
  SL --> GW
  WA --> GW
  More --> GW
  GW --> Gateway
  Gateway --> Msg
  Msg --> Engine
  Engine --> Out
  Out --> Platforms
  Sec --> Sessions
  Thread --> Sessions

When an external platform sends a webhook (or the channel polls for updates), the gateway adapter receives the HTTP request, the messaging adapter normalizes it into an internal event, and the agent engine processes it. Responses flow back through the outbound adapter, which formats them for the specific platform.

Decision Points

DecisionWhat controls itWhy it matters
Which adapters to implementPlatform capabilities & capabilities flagsNot every platform supports threads, reactions, or media — implement only what the platform offers
Webhook vs. pollinggateway adapter implementationTelegram uses long-polling by default; Discord uses gateway WebSocket; Slack uses HTTP webhooks
DM security policysecurity adapter & allowlist adapterControls who can DM the bot and which groups it participates in
Message formattingoutbound adapter & meta.markdownCapableSome platforms support Markdown; others need plain text or rich card formats
Thread modelthreading adapter & capabilities.threadsDetermines whether replies create threads, inline replies, or flat messages

The ChannelPlugin Interface

Core Structure

The ChannelPlugin type is a generic interface with 15+ optional adapter slots. Only id, meta, capabilities, and config are required:

type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = {
  // Identity
  id: ChannelId;
  meta: ChannelMeta;
  capabilities: ChannelCapabilities;

  // Configuration (required)
  config: ChannelConfigAdapter<ResolvedAccount>;
  configSchema?: ChannelConfigSchema;

  // Setup & Pairing
  setup?: ChannelSetupAdapter;
  setupWizard?: ChannelPluginSetupWizard;
  pairing?: ChannelPairingAdapter;

  // Security & Access Control
  security?: ChannelSecurityAdapter<ResolvedAccount>;
  approvals?: ChannelApprovalAdapter;
  allowlist?: ChannelAllowlistAdapter;

  // Messaging (inbound & outbound)
  messaging?: ChannelMessagingAdapter;
  outbound?: ChannelOutboundAdapter;

  // Groups & Threading
  groups?: ChannelGroupAdapter;
  threading?: ChannelThreadingAdapter;

  // Gateway & HTTP
  gateway?: ChannelGatewayAdapter<ResolvedAccount>;
  gatewayMethods?: string[];

  // Status & Health
  status?: ChannelStatusAdapter<ResolvedAccount, Probe, Audit>;
  heartbeat?: ChannelHeartbeatAdapter;

  // Additional Adapters
  auth?: ChannelAuthAdapter;
  mentions?: ChannelMentionAdapter;
  elevated?: ChannelElevatedAdapter;
  commands?: ChannelCommandAdapter;
  lifecycle?: ChannelLifecycleAdapter;
  streaming?: ChannelStreamingAdapter;
  directory?: ChannelDirectoryAdapter;
  resolver?: ChannelResolverAdapter;
  actions?: ChannelMessageActionAdapter;

  // Agent Integration
  agentPrompt?: ChannelAgentPromptAdapter;
  agentTools?: ChannelAgentToolFactory | ChannelAgentTool[];

  // Reload
  reload?: { configPrefixes: string[]; noopPrefixes?: string[] };
  defaults?: { queue?: { debounceMs?: number } };
};

Channel Metadata

The meta object provides user-facing information for the channel selection UI, setup wizards, and documentation links:

type ChannelMeta = {
  id: ChannelId;
  label: string;                    // "Telegram"
  selectionLabel: string;           // "Telegram Bot"
  docsPath: string;                 // "/channels/telegram"
  blurb: string;                    // One-line description
  order?: number;                   // Sort order in UI lists
  aliases?: readonly string[];      // Alternative names for matching
  markdownCapable?: boolean;        // Whether the platform renders Markdown
  showConfigured?: boolean;         // Show a "configured" badge in UI
  quickstartAllowFrom?: boolean;    // Allow in quickstart wizard
  systemImage?: string;             // Platform icon identifier
};

Capabilities Declaration

The capabilities object tells the core which features this channel supports, preventing the agent from attempting unsupported operations:

type ChannelCapabilities = {
  chatTypes: Array<ChatType | "thread">;  // "direct", "group", "channel", "thread"
  polls?: boolean;
  reactions?: boolean;
  edit?: boolean;              // Can edit sent messages
  unsend?: boolean;            // Can delete sent messages
  reply?: boolean;             // Can reply to specific messages
  effects?: boolean;           // Visual effects (confetti, etc.)
  groupManagement?: boolean;   // Can manage group membership
  threads?: boolean;           // Supports threaded conversations
  media?: boolean;             // Supports image/video/audio
  nativeCommands?: boolean;    // Supports platform-native slash commands
  blockStreaming?: boolean;    // Disable streaming for this channel
};
Key detail: The capabilities object is checked before every agent action. If a channel sets threads: false, the agent will never attempt to create a thread even if the user asks for one.

Adapter Architecture

flowchart TD
  subgraph Required["Required Adapters"]
    Meta["meta: ChannelMeta"]
    Caps["capabilities: ChannelCapabilities"]
    Config["config: ChannelConfigAdapter"]
  end
  subgraph Inbound["Inbound Path"]
    GW["gateway: ChannelGatewayAdapter"]
    Messaging["messaging: ChannelMessagingAdapter"]
    Security["security: ChannelSecurityAdapter"]
    Allowlist["allowlist: ChannelAllowlistAdapter"]
  end
  subgraph Outbound["Outbound Path"]
    Out["outbound: ChannelOutboundAdapter"]
    Threading["threading: ChannelThreadingAdapter"]
    Streaming["streaming: ChannelStreamingAdapter"]
  end
  subgraph Lifecycle["Lifecycle"]
    Setup["setup: ChannelSetupAdapter"]
    Pairing["pairing: ChannelPairingAdapter"]
    Status["status: ChannelStatusAdapter"]
    Heartbeat["heartbeat: ChannelHeartbeatAdapter"]
  end
  GW --> Security
  Security --> Allowlist
  Allowlist --> Messaging
  Messaging -->|Agent processes| Out
  Out --> Threading
  Threading --> Streaming

Adapters form a pipeline: inbound webhooks flow through the gateway, security, and messaging adapters before reaching the agent. Outbound responses flow through the outbound, threading, and streaming adapters before reaching the platform API.

Message Lifecycle

sequenceDiagram
  participant Platform as Telegram API
  participant GW as Gateway Adapter
  participant Sec as Security Adapter
  participant Msg as Messaging Adapter
  participant Agent as Agent Engine
  participant Out as Outbound Adapter
  participant Thread as Threading Adapter

  Platform->>GW: POST /webhook/telegram (update)
  GW->>GW: Validate webhook signature
  GW->>Sec: Check sender permissions
  Sec->>Sec: Verify allowlist & DM policy
  Sec-->>Msg: Authorized
  Msg->>Msg: Normalize to internal event
  Msg->>Agent: Process message
  Agent->>Agent: Run LLM, tools, etc.
  Agent->>Out: Send reply
  Out->>Thread: Resolve thread/reply-to
  Thread->>Platform: POST /sendMessage (formatted)
  Platform-->>GW: 200 OK

Annotated Code Walkthrough

Build a minimal channel extension step by step:

Define the channel plugin with metadata, capabilities, and the config adapter:

// extensions/my-channel/src/channel.ts
import type { ChannelPlugin, ChannelCapabilities, ChannelMeta } from "openclaw/plugin-sdk/core";

const meta: ChannelMeta = {
  id: "my-channel",
  label: "My Channel",
  selectionLabel: "My Channel Bot",
  docsPath: "/channels/my-channel",
  blurb: "Connect to My Channel messaging platform",
  markdownCapable: true,
};

const capabilities: ChannelCapabilities = {
  chatTypes: ["direct", "group"],
  reactions: true,
  edit: true,
  reply: true,
  threads: false,
  media: true,
};

export const myChannelPlugin: ChannelPlugin = {
  id: "my-channel",
  meta,
  capabilities,
  config: {
    resolve: (rawConfig) => ({
      botToken: rawConfig.myChannelBotToken,
      webhookUrl: rawConfig.myChannelWebhookUrl,
    }),
    validate: (resolved) => {
      if (!resolved.botToken) return { valid: false, error: "Bot token required" };
      return { valid: true };
    },
  },
};

Add the gateway adapter to handle inbound webhooks from the platform:

// extensions/my-channel/src/channel.ts (continued)
myChannelPlugin.gateway = {
  methods: ["POST"],
  handle: async (req, account) => {
    const body = await req.json();
    const event = normalizeInboundEvent(body);

    if (event.type === "message") {
      return {
        kind: "message",
        from: event.senderId,
        text: event.text,
        chatId: event.chatId,
        chatType: event.isGroup ? "group" : "direct",
      };
    }
    return { kind: "ignored" };
  },
};

myChannelPlugin.gatewayMethods = ["my-channel-webhook"];
Security note: Always validate webhook signatures in the gateway adapter. Unsigned webhooks allow message injection attacks.

Add the outbound adapter to send agent responses back to the platform:

// extensions/my-channel/src/channel.ts (continued)
myChannelPlugin.outbound = {
  send: async (ctx) => {
    const { account, target, message } = ctx;
    const response = await fetch(`https://api.mychannel.com/sendMessage`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${account.botToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        chat_id: target.id,
        text: message.text,
        reply_to: message.replyTo ?? undefined,
      }),
    });
    return { success: response.ok, messageId: (await response.json()).id };
  },
};

Register the channel via defineChannelPluginEntry():

// extensions/my-channel/index.ts
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { myChannelPlugin } from "./src/channel.js";

export default defineChannelPluginEntry({
  id: "my-channel",
  name: "My Channel",
  description: "My Channel messaging integration",
  plugin: myChannelPlugin,
});

Adapter Reference

AdapterTypePurpose
configChannelConfigAdapterResolve and validate channel configuration (tokens, URLs, options)
gatewayChannelGatewayAdapterHandle inbound HTTP webhooks and polling from the platform
messagingChannelMessagingAdapterNormalize inbound messages into internal events
outboundChannelOutboundAdapterFormat and send outbound messages to the platform API
securityChannelSecurityAdapterDM policies, sender verification, access control
allowlistChannelAllowlistAdapterUser/group allowlist for message filtering
approvalsChannelApprovalAdapterExecution approval workflows (e.g., approve tool calls via DM)
threadingChannelThreadingAdapterThread creation, reply-to resolution, thread context
groupsChannelGroupAdapterGroup membership management, group policy enforcement
statusChannelStatusAdapterHealth probes, audit logging, connection status
heartbeatChannelHeartbeatAdapterPeriodic keepalive pings to the platform
authChannelAuthAdapterToken refresh, OAuth flows, credential management
setupChannelSetupAdapterInitial channel setup and account linking
streamingChannelStreamingAdapterReal-time message streaming (edit-in-place typing indicators)
agentPromptChannelAgentPromptAdapterInject channel-specific context into the agent system prompt
agentToolsChannelAgentTool[]Channel-specific tools available to the agent (e.g., “send sticker”)

Edge Cases & Error Handling

ScenarioBehaviorAdapter
Webhook signature invalidReturn 401; event silently dropped; logged as warninggateway
Sender not in allowlistMessage silently ignored; no response sent to usersecurity / allowlist
Platform API rate limit (429)Outbound adapter retries with exponential backoff; messages queuedoutbound
Message too long for platformOutbound adapter splits into multiple messages or truncates with “...”outbound
Bot token expiredauth adapter attempts refresh; if failed, channel marked as unhealthyauth / status
Platform sends unsupported event typeGateway returns { kind: "ignored" }; no processing occursgateway
Group policy violationMessage rejected with policy error sent back to groupgroups / security

Real-World: Telegram Channel

The Telegram extension is one of the most feature-complete channels. Its api.ts barrel exports 25+ modules covering every aspect of the Telegram Bot API:

// extensions/telegram/api.ts (representative exports)
export * from "./src/accounts.js";           // Account management
export * from "./src/action-threading.js";   // Thread/reply handling
export * from "./src/allow-from.js";         // Allowlist configuration
export * from "./src/api-fetch.js";          // Telegram API HTTP client
export * from "./src/exec-approvals.js";     // Tool execution approval via DM
export * from "./src/group-policy.js";       // Group participation rules
export * from "./src/inline-buttons.js";     // Inline keyboard buttons
export * from "./src/model-buttons.js";      // Model selection buttons
export * from "./src/outbound-adapter.js";   // Message sending
export * from "./src/sticker-cache.js";      // Sticker metadata caching
export * from "./src/targets.js";            // Chat/user target resolution
export * from "./src/update-offset-store.js"; // Long-polling offset tracking
Pattern to follow: Telegram’s modular structure (one file per concern, barrel export in api.ts) is the recommended pattern for complex channel extensions with many features.

Configuration

Config KeyDefaultDescription
meta.id—Unique channel identifier (must match entry point id)
capabilities.chatTypes["direct"]Supported chat modes (direct, group, channel, thread)
defaults.queue.debounceMs0Message queue debounce (batch rapid messages into one agent call)
reload.configPrefixes[]Config key prefixes that trigger channel hot-reload
meta.markdownCapablefalseWhether outbound messages can use Markdown formatting

See also:

Directory Map

Source files for channel extensions
extensions/
├── telegram/
│   ├── index.ts                   ← Entry: defineChannelPluginEntry
│   ├── api.ts                     ← Public barrel (25+ exports)
│   └── src/
│       ├── channel.ts             ← ChannelPlugin implementation
│       ├── runtime.ts             ← Runtime dependency injection
│       ├── accounts.ts            ← Account binding & management
│       ├── outbound-adapter.ts    ← Message sending via Bot API
│       ├── inline-buttons.ts      ← Interactive keyboard buttons
│       ├── group-policy.ts        ← Group participation rules
│       ├── sticker-cache.ts       ← Sticker metadata caching
│       └── update-offset-store.ts ← Long-polling offset tracking
├── discord/
│   ├── index.ts                   ← Entry: Discord channel plugin
│   └── src/                       ← Discord-specific adapters
├── slack/
│   ├── index.ts                   ← Entry: Slack channel plugin
│   └── src/                       ← Slack Blocks, App Home, threads
├── whatsapp/
│   ├── index.ts                   ← Entry: WhatsApp Business API
│   └── src/                       ← Templates, media, receipts
├── signal/
│   ├── index.ts                   ← Entry: Signal Messenger
│   └── src/                       ← E2E encryption, groups
├── matrix/
│   ├── index.ts                   ← Entry: Matrix federated protocol
│   └── src/                       ← Rooms, E2E, federation
└── (15+ more channel extensions...)