Channels let an agent receive and send messages outside the web UI. Connect Telegram, Slack, Discord, WhatsApp, Signal, SMS, or any of the other supported providers, and the agent can reply to incoming messages just like it does in chat, with tools, memory, delegation, and signals all working the same way.
Spaces and agents
Each channel belongs to a space and is bound to a single agent. Conversations on the channel live inside that space, so the agent has access to the same memory and context it would have in the web UI for that space.
You can connect multiple channels to the same agent (for example, your personal assistant on both Telegram and SMS). You can also split channels by space: a finance space with its own SMS channel for bank alerts, a personal space with Telegram for everyday chat.
Dispatch modes
A channel can run in one of two modes:
| Mode | What it does | When to use |
|---|---|---|
| Message (default) | Inbound messages run full inference. The agent reads, replies, and uses tools. | Normal conversation channels. |
| Signal | Inbound messages do tag-only inference and feed the signal matcher. The agent doesn't reply. | Channels for ingesting alerts, 2FA codes, or events that other agents are waiting on. |
A channel can be flipped from one mode to the other in its settings. The two-tier authorization (see below) lets you mix the behaviors per sender.
Pairing and self-source
Channels require pairing before they accept inbound messages. The flow is:
- After connecting, the channel goes to Pairing state and shows a one-time code.
- You text the code from the address you want to use to the channel's number (or send the code from the linked account, depending on the provider).
- The platform records that address as a paired address on the channel.
- From then on, messages from any paired address are treated as you (self-source) and pass the default
receive_messagepolicy.
Messages from non-paired addresses are dropped by default. If you want the agent to talk to other people on the channel, you add a policy that allows specific contacts or addresses.
Authorization
Two policy actions gate every inbound:
| Action | Effect |
|---|---|
receive_message | Allows the agent to read and reply to the message |
receive_signal | Allows the message to be evaluated against active signal watches |
The dispatcher evaluates both:
- Both allowed → full inference. The agent replies and may use tools.
receive_signalallowed butreceive_messagedenied → tag-only inference. The agent annotates the message for signal matching but cannot reply or use general tools.- Both denied → the message is discarded. No DB record, no broadcast.
This is how you get behaviors like "the finance agent never sees SMS at all" or "this channel is for ingesting bank 2FA codes only".
See Policies for the full action reference.
Interactive prompts on channels
When an agent needs your input (an app deploy approval, a multiple-choice question, a credential pick), it pauses the chat and asks. How that prompt arrives depends on the channel:
| Channel kind | Render |
|---|---|
| Telegram, Discord, Slack, WhatsApp Cloud | Real buttons (inline keyboard, Block Kit, interactive message). Tap one and the agent resumes. |
| Signal, WhatsApp Personal, SMS | A prompt followed by "Reply YES or NO". Quote-reply with yes, y, ok, 👍, no, n, nope, ❌ (and many variants). The parser is generous. |
The chat resumes the moment all pending prompts on the latest turn are resolved. Until then, the agent doesn't generate or call other tools.
For credential picks, channels don't render the vault picker inline. The prompt links to a web page where you choose the entry — same chat-pause behavior.
Message length and formatting
Each adapter splits long replies at safe boundaries — paragraph breaks, then line breaks, then word boundaries — so a single agent turn that exceeds a platform limit arrives as a sequence of clean chunks rather than getting truncated:
| Channel | Per-message limit | Notes |
|---|---|---|
| Telegram | 4,096 chars | Markdown is rendered into MarkdownV2. Tables render as monospace code blocks. |
| Discord | 2,000 chars | Markdown is rendered into Discord's flavor. Tables render as monospace code blocks. |
| Slack | Plain text | The adapter multi-chunks silently. |
| SMS | 1,600 chars | Long replies are truncated and finished with Full reply: {short-link} pointing at a share URL for the full conversation. |
| WhatsApp (both) | Per-message limit | Markdown is converted to WhatsApp's inline styles. |
| Signal | Per-message limit | Markdown is converted to Signal's BodyRange format. |
Splitters never break inside a code fence or between escape character and the character it escapes, so syntax stays intact.
Available adapters
| Channel | Connection | Public URL? | Best for |
|---|---|---|---|
| Telegram | Webhook from Telegram | Yes | Personal assistants, group bots |
| Slack | Socket Mode (outbound WebSocket) | No | Team workspaces |
| Discord | Gateway WebSocket | No | Community servers, gaming groups |
| WhatsApp (Cloud API) | Meta webhook | Yes | Business numbers, customer-facing |
| WhatsApp (Personal) | QR linked device | No | Personal use, testing |
| Signal | QR linked device | No | Personal use, 2FA capture |
| SMS | Twilio webhook | Yes | Mobile assistants, 2FA capture, notifications |
Channel statuses
| Status | Meaning |
|---|---|
| Setup | The channel is created but is waiting for an interactive step: for example scanning a QR code (Signal, WhatsApp Personal) or verifying a webhook in the provider's dashboard (WhatsApp Cloud API). |
| Pairing | Waiting for the user to complete address pairing (e.g., text the pairing code to the bot) |
| Connecting | The adapter is starting and registering with the provider |
| Connected | The channel is live and receiving messages |
| Disconnected | The channel is stopped (you turned it off) |
| Failed | The adapter could not start. See the error message on the channel page. |
Retry policy
When a channel fails to start or its connection drops, Frona retries automatically with exponential backoff. The default policy lives in channel.retry in the config file:
| Field | Default | Meaning |
|---|---|---|
max_retries | unlimited | Total retry attempts before the channel is marked Failed for good. |
initial_backoff_ms | 1000 | Delay before the first retry. |
backoff_multiplier | 2.0 | Delay multiplier between attempts. |
max_backoff_ms | 60000 | Cap on the delay between attempts. |
Every channel can override these defaults on its detail page (Retry policy section). Per-channel values take precedence over the server default. If you want a channel to fail fast (say, a Slack bot that should never silently retry), set max_retries to a small number for that channel.
Tips
- Start in Message mode. Switch to Signal mode only when you specifically want passive watching, not conversation.
- Pair from the address you'll use. The pairing code locks the channel to that wire address. If you want to add another, repeat pairing.
- Use a separate agent per channel if the channels have different audiences. The agent's specialty is what people on the other end interact with.
- Use spaces to scope memory. A channel for work and a channel for personal life should live in different spaces so memory doesn't leak between them.
- Watch the status. A channel stuck in
Failedwon't receive anything. Check the error message before debugging at the provider.