Skip to main content
To connect a platform beyond the built-in Feishu and Discord, subclass ChannelBase to write a channel class and hand it to create_app. A channel class is the translation layer between the IM platform and the agent service: all platform differences are contained here, and the framework handles the orchestration. A channel class does two things: describe its type (declare the type id, credentials, and config) and implement its behavior (keep the connection, normalize inbound messages and emit them, send replies back to the platform).

Type description

A channel class carries its type information on the class itself, so the framework renders the frontend form, validates input, and constructs instances from it, with no separate registry.
A self-describing channel class
The framework builds every instance with a uniform (channel_id, credentials, config): credentials and config are already validated against the Credentials / Config you declared. Credentials holds secrets (encrypted, redacted, immutable); Config holds non-secret switches (plaintext, updatable); users fill in both per channel in the management UI.

Required methods

Besides the constructor, ChannelBase has three abstract methods you must implement; the rest have sensible defaults (a no-op or “not supported”).

Receive messages

The framework calls start_listening(emit) when it starts the channel, passing in the inbound callback emit. The channel stores it as self._emit, normalizes each platform message into a ChannelEvent, and calls it; the framework handles the rest. A channel class neither holds nor imports the orchestration layer.
Store emit and normalize inbound messages
ChannelEvent.content reuses the same TextBlock / DataBlock types as Msg.content, so multimodal messages reach the agent with no extra conversion.

Send replies

The framework doesn’t hand the channel a finished reply; it hands send_response the run’s event stream and lets the channel accumulate, render, and send. That way a channel can send one complete reply, or stream updates as Feishu does. The method has two parameters: event is the send target, used only for its chat_id to locate which chat to reply to; events is the agent event stream this run produces. The base class provides _render(), which folds an accumulated reply into deliverable text / data blocks and, per the channel’s display switches, decides whether to include the thinking process and tool calls.
Accumulate the event stream and send it back
For a complete streaming and error-handling implementation, see the built-in FeishuChannel and DiscordChannel.

Connection status

Each channel instance creates its own self.status = ChannelStatus() in __init__ and updates status.state as it connects, reconnects, and stops (values stopped / connecting / connected / retrying / failed). This is exactly what the management UI and GET /channels/{id}/status read. If the first connection keeps failing, you can set state to failed and park, waiting for the user to change the config before reconnecting.

Capability declaration

capabilities is the channel’s declaration of what the platform supports, used by the framework and the channel itself when sending. Declare it truthfully for your platform:
Declare capabilities

Tool confirmation

When an agent calls a tool that needs approval, the run pauses and a RequireUserConfirmEvent appears in the event stream. The channel recognizes it in send_response and presents the request to the user: capable platforms use an interactive card or buttons, and a plain-text platform can send a “reply yes/no” prompt. RequireUserConfirmEvent.tool_calls is the list of tools awaiting approval, each with id, name, and input. Once the user decides, the channel normalizes it into a ChannelConfirmationResultEvent and emits it through the same self._emit entry as regular messages:
Present the confirmation and return the decision
ChannelConfirmationResultEvent carries only lookup keys: the authoritative awaiting tool call is read from session state on resume, never trusted from the card. This makes the round-trip distributed-safe by nature, no matter how long the user waits to click or which node the click lands on.
When you present the confirmation, the event.metadata that send_response received carries the run’s agent_id and session_id. Include them in the ChannelConfirmationResultEvent so a click resumes exactly the session that asked for approval, with no need to re-match routing rules.

Optional methods

These all have default implementations; override them per your platform’s capabilities:

Enable the channel

Add your channel class to create_app’s channels, and the service accepts that platform. channels declares which channel types the whole service accepts; omitting it enables none, so list the built-in types and your custom types together.
Enable in create_app
Once enabled, MyChannel shows up in the platform-type list of the management UI, and users can fill in credentials, configure routing, and create channels just like a built-in platform.

Further reading

Message routing

Custom channels reuse the same routing model.

Overview

Back to how channels work overall.