Bridge external messaging platforms to OpenClaw’s agent engine through a uniform adapter interface.
Channel Extensions
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
Inbound webhooks/polling from platforms, outbound messages from the agent, config with bot tokens.
Which adapters to implement, capability flags, security policy, threading model, gateway method registration.
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
| Decision | What controls it | Why it matters |
|---|---|---|
| Which adapters to implement | Platform capabilities & capabilities flags | Not every platform supports threads, reactions, or media — implement only what the platform offers |
| Webhook vs. polling | gateway adapter implementation | Telegram uses long-polling by default; Discord uses gateway WebSocket; Slack uses HTTP webhooks |
| DM security policy | security adapter & allowlist adapter | Controls who can DM the bot and which groups it participates in |
| Message formatting | outbound adapter & meta.markdownCapable | Some platforms support Markdown; others need plain text or rich card formats |
| Thread model | threading adapter & capabilities.threads | Determines 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
};
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"];
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
| Adapter | Type | Purpose |
|---|---|---|
config | ChannelConfigAdapter | Resolve and validate channel configuration (tokens, URLs, options) |
gateway | ChannelGatewayAdapter | Handle inbound HTTP webhooks and polling from the platform |
messaging | ChannelMessagingAdapter | Normalize inbound messages into internal events |
outbound | ChannelOutboundAdapter | Format and send outbound messages to the platform API |
security | ChannelSecurityAdapter | DM policies, sender verification, access control |
allowlist | ChannelAllowlistAdapter | User/group allowlist for message filtering |
approvals | ChannelApprovalAdapter | Execution approval workflows (e.g., approve tool calls via DM) |
threading | ChannelThreadingAdapter | Thread creation, reply-to resolution, thread context |
groups | ChannelGroupAdapter | Group membership management, group policy enforcement |
status | ChannelStatusAdapter | Health probes, audit logging, connection status |
heartbeat | ChannelHeartbeatAdapter | Periodic keepalive pings to the platform |
auth | ChannelAuthAdapter | Token refresh, OAuth flows, credential management |
setup | ChannelSetupAdapter | Initial channel setup and account linking |
streaming | ChannelStreamingAdapter | Real-time message streaming (edit-in-place typing indicators) |
agentPrompt | ChannelAgentPromptAdapter | Inject channel-specific context into the agent system prompt |
agentTools | ChannelAgentTool[] | Channel-specific tools available to the agent (e.g., “send sticker”) |
Edge Cases & Error Handling
| Scenario | Behavior | Adapter |
|---|---|---|
| Webhook signature invalid | Return 401; event silently dropped; logged as warning | gateway |
| Sender not in allowlist | Message silently ignored; no response sent to user | security / allowlist |
| Platform API rate limit (429) | Outbound adapter retries with exponential backoff; messages queued | outbound |
| Message too long for platform | Outbound adapter splits into multiple messages or truncates with “...” | outbound |
| Bot token expired | auth adapter attempts refresh; if failed, channel marked as unhealthy | auth / status |
| Platform sends unsupported event type | Gateway returns { kind: "ignored" }; no processing occurs | gateway |
| Group policy violation | Message rejected with policy error sent back to group | groups / 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
api.ts) is the recommended pattern for complex channel extensions with many features.
Configuration
| Config Key | Default | Description |
|---|---|---|
meta.id | — | Unique channel identifier (must match entry point id) |
capabilities.chatTypes | ["direct"] | Supported chat modes (direct, group, channel, thread) |
defaults.queue.debounceMs | 0 | Message queue debounce (batch rapid messages into one agent call) |
reload.configPrefixes | [] | Config key prefixes that trigger channel hot-reload |
meta.markdownCapable | false | Whether outbound messages can use Markdown formatting |
Related Topics
See also:
- Extensions overview — all extension categories and the full ecosystem
- Extension Contract — the shared registration interface used by
defineChannelPluginEntry() - Provider Adapters — the parallel adapter pattern for AI providers
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...)