Skip to content

Building a Channel Adapter

Built-in channel adapters bridge external messaging platforms through the Unified Message Interface (UMI). Gateway clients use a separate WebSocket frame schema; adapters are in-process classes.

External Platform (e.g., Slack, Teams, custom platform)
│
▼
YourChannel extends BaseChannel (your code)
│
├── emitMessage() ──► UMI ──► Gateway Hub ──► Root agent
│
◄── send() ◄── UMI ◄── Gateway Hub ◄── Root agent response
◄── setReaction() ◄── UMI ◄── Gateway Hub ◄── Feedback emoji
◄── sendTyping() ◄── UMI ◄── Gateway Hub ◄── Typing indicator

The following is SDK-specific pseudocode, not a drop-in implementation. Define and type MyPlatformClient, load credentials through the config/vault integration, and map real message IDs and error handling before using it.

src/channels/my-platform/index.ts
import { BaseChannel } from '../interface';
import type { ChannelResponse, ChannelType } from '@/core/types';
export class MyPlatformChannel extends BaseChannel {
readonly type: ChannelType = 'my-platform';
readonly name = 'My Platform';
private client: MyPlatformClient | null = null;
async connect(): Promise<void> {
// Initialize your platform SDK
// Resolve credentials through the vault/config integration used by the
// rest of the backend. Do not read channel secrets from process.env at
// runtime; .env is only a first-boot seed for configured deployments.
this.client = new MyPlatformClient(await loadMyPlatformCredentials());
// Bridge: platform messages → channel
this.client.on('message', (msg) => {
this.emitMessage(
this.createUnifiedMessage(msg.channelId, msg.author.id, msg.content, {
userName: msg.author.username,
metadata: { messageId: msg.id },
})
);
});
await this.client.connect();
this.setConnected(true);
}
async disconnect(): Promise<void> {
await this.client?.disconnect();
this.setConnected(false);
}
// Channel sends response → deliver to platform
async send(channelId: string, response: ChannelResponse): Promise<string> {
await this.client.sendMessage(channelId, response.content);
return 'message-sent';
}
// Optional: emoji reactions
async setReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
await this.client.addReaction(messageId, emoji);
}
// Optional: typing indicator
async sendTyping(channelId: string, active: boolean = true): Promise<void> {
if (active) {
await this.client.startTyping(channelId);
}
}
}

Export a BaseChannel instance or a constructible subclass from src/channels/<name>/index.ts. src/channels/discovery.ts discovers it; src/channels/index.ts registers discovered channels with UMI. Override isEnabled(config) to gate startup on configured credentials, following a sibling adapter. Register any new settings and secret mappings, and verify the channel is also present in the production bundle.

Add your channel type to src/core/types.ts:

// Append 'my-platform' to the existing ChannelType union.
// Preserve existing API/test channel members.

And update the configuration schema in src/config/schema.ts to include settings for your channel if needed.

Your channel must implement these abstract methods from BaseChannel:

connect(): Promise<void> — Initialize the channel

async connect(): Promise<void> {
// Set up client, register event listeners, connect to platform
this.setConnected(true);
}

disconnect(): Promise<void> — Clean up and disconnect

async disconnect(): Promise<void> {
// Close connections, clear state
this.setConnected(false);
}

send(channelId: string, response: ChannelResponse): Promise<string> — Deliver a response to the user

async send(channelId: string, response: ChannelResponse): Promise<string> {
// response.content — message text
// response.attachments — array of files to send
// response.replyTo — optional message ID to reply to
// response.threadId — optional thread to post in
// Return the platform-specific message ID
}

setReaction(channelId: string, messageId: string, emoji: string): Promise<void> — Add emoji reaction

async setReaction(channelId: string, messageId: string, emoji: string): Promise<void> {
// Add emoji to a message (skip if platform doesn't support)
}

sendTyping(channelId: string, active: boolean): Promise<void> — Typing indicator

async sendTyping(channelId: string, active: boolean = true): Promise<void> {
// Show/hide typing indicator (skip if platform doesn't support)
}

When a user sends a message on your platform, emit it to the UMI:

this.emitMessage(
this.createUnifiedMessage(
'channel-123', // Platform-specific chat/channel ID
'user-456', // Platform user ID
'Hello!', // Message text
{
userName: 'Alice', // Display name (optional)
attachments: [], // Optional file attachments
threadId: 'thread-1', // Optional thread/reply chain
metadata: { messageId: 'msg-789' }, // Platform-specific metadata
}
)
);

Update the connection state when the channel connects or disconnects:

this.setConnected(true); // Connected
this.setConnected(false); // Disconnected

To emit errors, use:

this.emitError(new Error('Connection failed'));

Test your channel by subscribing to UMI events:

import { MyPlatformChannel } from './my-platform';
import { getUMI } from '@/channels';
const channel = new MyPlatformChannel();
const umi = getUMI();
umi.register(channel);
// Listen for messages from the platform
const messages: any[] = [];
umi.on('message', (msg) => {
messages.push(msg);
});
await channel.connect();
// Simulate platform message
channel.emitMessage(
channel.createUnifiedMessage('ch-1', 'u-1', 'test', {
userName: 'Alice',
})
);
expect(messages).toHaveLength(1);
expect(messages[0].content).toBe('test');
await channel.disconnect();

Edit this page in the repository.