Skip to main content

Plugin WebSocket Protocol

Every channel plugin connects to the Codebolt server over a single persistent WebSocket connection on the /plugin endpoint. This connection carries all messages in both directions: gateway routing, replies, proactive messages, and lifecycle events.

Connecting

ws://<server>/plugin?pluginId=<id>&pluginToken=<token>

The Plugin SDK handles this automatically. When your plugin starts, the SDK establishes the connection and authenticates using the pluginId and pluginToken assigned by the server when it spawns the plugin process.

You do not need to open or manage this connection manually — it is part of @codebolt/plugin-sdk.

Message flow overview

Plugin process Server (pluginSocket.ts)
│ │
│── connect /plugin?id=X ──────▶│ token validation
│◀── pluginStartMessage ────────│ sends manifest + config
│ │
│── gateway.route ─────────────▶│ routeMessage()
│◀── gateway.routeAck ──────────│ RoutingResult
│ │
│── gateway.registerChannel ───▶│ ChannelStore.create()
│◀── gateway.registerAck ───────│ channelId
│ │
│ (agent runs…) │
│ │
│◀── gateway.reply ─────────────│ agent sendMessage() → plugin
│◀── gateway.messageToChannel ──│ proactive message → plugin
│ │
│── disconnect ─────────────────│ cleanup + unregister

Message types

All messages are JSON objects with a type field.

Plugin → Server

TypePayloadDescription
gateway.routeGatewayMessageRoute an incoming platform message to an agent
gateway.registerChannelChannelConfigRegister this plugin as a channel
gateway.unregisterChannel{ channelId }Mark channel as disconnected
gateway.routeAckAcknowledge a received reply (internal)

Server → Plugin

TypePayloadDescription
pluginStartMessagemanifest + configSent immediately after connect
gateway.replyGatewayReplyAgent reply for the plugin to forward to the platform
gateway.messageToChannel{ targetId, text, metadata }Proactive message from an agent
gateway.routeAckRoutingResultResult of a gateway.route call

GatewayReply format

When the agent calls sendMessage(), the server delivers this to the plugin:

interface GatewayReply {
type: 'gateway.reply';
data: {
text: string;
replyTo: {
channelId: string;
externalThreadId?: string;
userId?: string;
};
metadata?: Record<string, any>;
attachments?: Array<{
type: 'image' | 'file';
url: string;
name?: string;
}>;
threadId: string;
instanceId: string;
timestamp: string;
};
}

Your plugin's plugin.gateway.onReply() handler receives the data object.

Receiving replies in your plugin

plugin.gateway.onReply(async (reply) => {
const { text, replyTo, attachments } = reply;

// Send text back to the platform
await platformApi.sendMessage(replyTo.externalThreadId, text);

// Handle attachments if present
for (const attachment of attachments ?? []) {
if (attachment.type === 'image') {
await platformApi.sendPhoto(replyTo.externalThreadId, attachment.url);
}
}
});

Connection lifecycle

The server tracks one WebSocket connection per plugin instance. On disconnect:

  1. The server removes the connection from pluginConnections
  2. Any channels registered by this plugin are marked disconnected in ChannelStore
  3. Pending replies to those channels are dropped
  4. The plugin process may be restarted by PluginProcessManager depending on its restart policy

Chunking long messages

Platform APIs impose character limits. The plugin (not the server) is responsible for chunking:

  • Discord: 2,000 characters per message
  • Slack: 4,000 characters per message block
  • Telegram: 4,096 characters per message

The first-party plugins handle chunking automatically. If you write a custom plugin, split on the platform's limit before calling the platform API.

See also