Custom Channel Plugins
If you want to connect Telegram, Slack, Discord, a support inbox, or another message surface to Codebolt, you build a plugin with manifest type "channel" and use the plugin.gateway module.
Minimal Manifest
{
"name": "my-channel-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"codebolt": {
"plugin": {
"type": "channel",
"triggers": [{ "type": "manual" }],
"ui": {
"path": "./ui/default/index.html"
}
}
}
}
Why this shape:
"type": "channel"enables the chat-gateway plugin patternmanualis usually the right trigger because channel plugins often need credentials or operator settings firstui.pathgives you a dynamic panel for connection setup and status
Minimal Runtime Structure
import plugin from '@codebolt/plugin-sdk';
plugin.onStart(async () => {
plugin.gateway.onReply(async (reply) => {
await sendReplyToPlatform(reply.externalThreadId, reply.text);
});
plugin.gateway.onMessageToChannel(async (message) => {
await sendOutboundMessage(message.targetId, message.text);
});
});
async function connect(config: {
agentId: string;
threadStrategy: 'single' | 'per-user' | 'per-conversation' | 'per-message' | 'existing';
}) {
await plugin.gateway.registerChannel({
name: 'My Platform',
platform: 'my-platform',
agentId: config.agentId,
threadStrategy: config.threadStrategy,
});
platformClient.onMessage(async (incoming) => {
await plugin.gateway.routeMessage({
source: 'channel',
sourceId: 'plugin-my-platform',
threadStrategy: config.threadStrategy,
agentId: config.agentId,
text: incoming.text,
userId: incoming.userId,
externalThreadId: incoming.threadId,
replyTo: {
channelId: 'my-platform',
externalThreadId: incoming.threadId,
userId: incoming.userId,
},
metadata: {
senderName: incoming.senderName,
rawPayload: incoming.rawPayload,
},
timestamp: incoming.timestamp,
});
});
}
plugin.onStop(async () => {
await plugin.gateway.unregisterChannel();
});
What Each Gateway Method Is For
| Method | Use it for |
|---|---|
registerChannel(...) | create or refresh the routing-rule entry for the connected platform |
routeMessage(...) | hand an inbound external message to the server gateway |
onReply(...) | receive agent replies tied to a routed conversation |
onMessageToChannel(...) | receive proactive outbound messages from Codebolt |
unregisterChannel() | mark the channel disconnected when the plugin stops |
Recommended Plugin Shape
In practice, a production channel plugin usually has four layers:
- platform adapter: Slack SDK, Telegram bot, Discord client, webhook listener, or another transport
- gateway bridge: converts external messages into
plugin.gateway.routeMessage(...) - config and persistence: stores tokens, agent choice, and strategy in
plugin.kvStore - dynamic panel UI: lets operators connect, disconnect, inspect status, and choose routing settings
That is the exact pattern used by the Telegram plugin.
Telegram Plugin Pattern
The built-in Telegram plugin is a good reference because it does not try to own routing itself. It lets the server do that work.
Its core structure is:
- load saved config from
plugin.kvStore - open the Telegram bot connection
- call
plugin.gateway.registerChannel(...) - forward inbound messages with
plugin.gateway.routeMessage(...) - send replies back with
plugin.gateway.onReply(...) - accept proactive outbound messages with
plugin.gateway.onMessageToChannel(...) - expose operator controls through a dynamic panel
It also does a few practical things you should copy:
- ignore messages from the bot itself
- keep
sourceIdstable - use
threadStrategyfrom saved config rather than hardcoding it - keep raw platform payload in
metadatawhen debugging or auditing matters
Channel Registration Data
When you register a channel, the server persists a channel config record. The important fields are:
| Field | Meaning |
|---|---|
name | operator-facing channel name in routing rules |
platform | external platform identifier such as telegram or discord |
agentId | default agent to run for inbound messages from this channel |
threadStrategy | how external identity is mapped to Codebolt threads |
environmentId | optional environment selection for the agent |
autoConnect | whether the channel should be treated as automatically connectable |
When To Use A Dynamic Panel
Use a plugin UI if the integration needs:
- tokens, secrets, or connection metadata
- selection of
agentIdorthreadStrategy - connect and disconnect controls
- status and validation feedback
For many channel plugins, the UI is not optional in practice. Operators need a reliable place to manage credentials and routing choices.
Common Mistakes
| Mistake | Better approach |
|---|---|
generating a fresh sourceId every time | keep a stable sourceId for the logical channel |
| storing thread maps in the plugin | let the server own routing persistence |
| replying directly from platform callbacks before routing completes | route first, then let onReply(...) handle the return path |
using startup when operator config is required | use manual, then auto-load saved config in onStart() if present |
skipping unregisterChannel() on shutdown | mark the channel disconnected cleanly |