Channels Reference
Channels are communication integrations such as Telegram, GitHub, Slack, Discord, Pancake, and Zalo. They translate provider webhooks into the shared agent input shape, then send replies through a channel-specific ChannelActions implementation.
Customers interact with the provider bot, app, or webhook. They do not receive account secrets. The webhook URL always includes the account, agent, and channel:
{AGENT_SERVICE_URL}/webhooks/{accountId}/{agentId}/{channel}
Runtime Flow
Webhook handling is split deliberately:
functions/harness-processing/integrations.tsowns routing, account/agent lookup, adapter selection, provider ACKs, and normalized channel events.functions/harness-processing/handler.tsowns session setup, command dispatch, agent execution, and final reply handling.functions/_shared/channels.tsowns the shared channel contracts.functions/_shared/<channel>-channel.tsowns provider-specific authentication, parsing, formatting, and reply API calls.
Supported Channels
| Channel | Adapter | Required config | Documentation |
|---|---|---|---|
telegram | functions/_shared/telegram-channel.ts | botToken, webhookSecret, allowedChatIds | Telegram Details |
github | functions/_shared/github-channel.ts | webhookSecret, appId, privateKey | GitHub Details |
slack | functions/_shared/slack-channel.ts | botToken, signingSecret | Slack Details |
discord | functions/_shared/discord-channel.ts | botToken, publicKey | Discord Details |
pancake | functions/_shared/pancake-channel.ts | pageId, pageAccessToken | Pancake Details |
zalo | functions/_shared/zalo-channel.ts | botToken, webhookSecret, allowedUserIds | Zalo Details |
Channel Contract
Each channel implements ChannelAdapter from functions/_shared/channels.ts:
| Method | Purpose |
|---|---|
name | Stable URL segment and config key, such as telegram |
canHandle(req) | Quick provider-shape check, usually based on headers |
authenticate(req) | Provider-native signature or secret verification |
parse(req) | Converts the webhook into message, ignore, or direct response |
actions(msg) | Returns reply, typing, and reaction actions scoped to the inbound message |
parse() returns one of three outcomes:
| Result | Meaning |
|---|---|
message | Continue into the agent loop after sending ack or a default 200 |
ignore | Stop without running the agent, usually for unsupported events |
response | Return a provider-specific response immediately, such as a challenge reply |
The normalized InboundMessage contains:
eventId: provider delivery/message ID used for deduplicationconversationKey: provider thread/chat/channel key used for persisted conversation statechannelName: adapter namecontent: Vercel AI SDKUserContentsource: provider metadata needed for commands, replies, or diagnostics
integrations.ts scopes eventId and conversationKey with accountId and agentId before the session sees them.
Add a Channel
- Add config types to
functions/_shared/storage/agent-config.ts. - Validate the new
config.channels.<channel>fields innormalizeChannelsConfig(). - Create
functions/_shared/<channel>-channel.ts. - Implement
ChannelAdapter. - Keep provider-specific reply formatting and send logic inside the channel module.
- Import the channel factory in
functions/harness-processing/integrations.ts. - Add
create<Channel>ChannelFromConfig()and include it increateChannelRegistry(). - Document the webhook URL as
/webhooks/{accountId}/{agentId}/{channel}. - Update the API Reference
AgentConfig.channelsschema,examples/account.config.example.json, setup scripts, and focused tests/examples when the public config changes.
Do not hardcode channel-specific behavior in commands, shared handlers, or the core agent loop. Commands receive only the channel-agnostic ChannelActions interface.
Adapter Skeleton
/**
* Example channel adapter implemented as a ChannelAdapter.
* Keep Example auth, message normalization, and reply actions here.
*/
import type { ChannelAdapter, ChannelParseResult } from "./channels.ts";
export function createExampleChannel(
token: string,
webhookSecret: string,
): ChannelAdapter {
return {
name: "example",
canHandle(req) {
return "x-example-delivery" in req.headers;
},
authenticate(req) {
return req.headers["x-example-secret"] === webhookSecret;
},
parse(req): ChannelParseResult {
const body = JSON.parse(req.body) as {
id: string;
threadId: string;
text?: string;
};
if (!body.text) {
return { kind: "ignore", response: { statusCode: 200 } };
}
return {
kind: "message",
ack: { statusCode: 200 },
message: {
eventId: body.id,
conversationKey: body.threadId,
channelName: "example",
content: [{ type: "text", text: body.text }],
source: body as Record<string, unknown>,
},
};
},
actions(msg) {
return {
sendText: async (text) => {
await fetch("https://api.example.com/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
threadId: msg.conversationKey,
text,
}),
});
},
sendTyping: async () => {},
reactToMessage: async () => {},
};
},
};
}
Channel Rules
- Verify provider signatures or webhook secrets before parsing user-controlled payloads deeply.
- Return a provider ACK quickly; long-running model work should happen in
afterResponse. - Use stable provider IDs for
eventIdso duplicate deliveries are deduped. - Use thread/chat/channel IDs for
conversationKeyso follow-up messages preserve context. - Put provider-specific Markdown or HTML formatting in the channel module.
- Keep
ChannelActionsmethods resilient; failed typing or reaction calls should not fail the whole turn. - Keep approval-dependent tools off channel-only agents unless a direct API client will resume the approval flow.