Skip to Content
Polyant is open source under AGPL-3.0 — star us on GitHub.
ConceptsChannels

Channels

On this page

A channel is the surface where an end user (or an external system, or another agent) meets a Polyant instance. Channels are interchangeable transports: the supervisor and the pipeline never care whether a turn arrived from WhatsApp or from the OpenAI-compatible HTTP API. Each channel is implemented as a ChannelAdapter that normalises traffic into an IncomingMessage going in and an OutgoingMessage going out.

This page covers the full list of message sources, how channel credentials are scoped, how messy fragmented bursts are collapsed before they hit the LLM, how voice notes are transcribed, and how the in-process agent channel powers agent-to-agent calls.

The eleven message sources

Polyant distinguishes two related but distinct concepts:

  • ChannelType - the narrow set of channels with stored, admin-API-configurable rows in instance_channels. The tuple is ["telegram", "slack", "whatsapp", "agent", "web", "http", "email", "hubspot", "voice"] (see packages/engine/src/instances/channels.store.ts). The agent entry is API-configurable because per-instance agent-to-agent settings live in instance_channels just like the network transports - but its “credentials” are routing metadata, not external tokens. The web entry has no adapter and no credentials either: its row’s enabled flag decides whether the agent’s public HTTP endpoints answer, and a missing row means off. http (the synchronous REST channel), email hubspot and voice are available only with managed services.
  • MessageChannelType - the wide superset, covering every possible source of an inbound pipeline message. It adds scheduled (cron-style task) and room (event-driven cycles) to the ChannelType entries.

Putting them together, the eleven message sources are:

channelTypeDirectionConfigurableTransport / Trigger
telegramInbound + outboundYes (per-instance)grammY webhook
slackInbound + outboundYes (per-instance)@slack/bolt Events API webhook
whatsappInbound + outboundYes (per-instance)Twilio Programmable Messaging webhook
webInbound + outboundYes (per-instance), off for a new agent; switch, key and context fields on the Web/API card/v1/chat/completions, /api/instances/{slug}/chat/stream; the Playground’s own route, which works with Web off
scheduledInbound (system)NoScheduler ticks
roomInbound (system)NoRoom scheduler (event-driven, 30s tick)
agentInbound + outboundYes (per-instance)In-process agent-to-agent invocation
httpInbound + outboundYes (per-instance)Synchronous REST call (managed services)
emailInbound + outboundYes (per-instance)Provider webhook in, SMTP out (managed services)
hubspotInbound + outboundYes (per-instance)HubSpot Conversations webhook in, REST out (managed services)
voiceInbound phone callsYes (per-instance)Twilio voice webhook and Media Streams WebSocket (managed services)

The supervisor handles all eleven the same way. The difference shows up only at the edges: which adapter received the message, and which adapter (if any) sends the reply.

Inbound from outside: the A2A server

There is one more way a turn can start, and it is not a channelType: an agent can be published to external Agent-to-Agent clients. With A2A enabled, clients discover it with GET at /a2a/{slug}/.well-known/agent-card.json and run a turn with POST at /a2a/{slug}/jsonrpc.

It is worth keeping distinct from the agent source above, because the names invite confusion:

agent channelA2A server
Who callsyour own agents, in-processany external A2A client, over HTTP
Boundarythe deploymentthe organization - a task belongs to the agent it was created on
Authinternal routingagent card is public; JSON-RPC follows the agent’s API-key setting like /v1/chat/completions

See A2A configuration and the route reference.

Per-instance credentials, encrypted at rest

There are no global channel credentials in .env. Every channel config lives in instance_channels, keyed by (instance_id, channel_type), with the JSON payload AES-256-GCM encrypted via the engine’s ENCRYPTION_KEY. The admin panel’s Channels tab is the canonical write surface; the same data is exposed via PUT /api/instances/:slug/channels/:type and DELETE /api/instances/:slug/channels/:type.

The ChannelManager reads enabled configs from the database, instantiates the right adapter (Telegram, Slack, WhatsApp), and starts it. Adapters can be started, stopped, and reconfigured at runtime - no engine restart required. Channel boot is fire-and-forget: a provider failure cannot block the rest of the engine.

Two failure modes are recorded rather than swallowed, because both used to be indistinguishable from a channel nobody had configured:

  • An adapter that fails to initialise disables its own channel, storing the adapter’s error text and the time. Without that, a bad credential meant a crash loop on every restart. Re-enabling the channel clears both fields.
  • A payload that will not decrypt is marked unreadable instead of being returned as an empty object. A rotated ENCRYPTION_KEY and a never-configured channel look identical otherwise, and they call for opposite repairs.

Both surface on the agent’s Status section, which is the only place that reports them.

Inbound message coordinator

Two of the human-facing channels - WhatsApp and Telegram - have a habit of arriving in fragments. A user types one thought across three quick bubbles, and three near-simultaneous webhooks land at the engine. Running the supervisor three times for what is conceptually one message wastes tokens and confuses the LLM.

The MessageCoordinator debounces fragmented bursts using a cancel-and-restart model:

  1. The first fragment arms a soft-debounce timer and a typing-indicator timer. Both windows belong to the AGENT - a support agent and a booking agent legitimately want different ones - set in its Behaviour overrides (Burst window and Typing indicator delay) and falling back to the shipped defaults of 2 s and 1.5 s.

    They are resolved once per burst, when the first fragment arrives, and held for it: a burst belongs to one conversation and therefore one agent, so the window cannot change halfway through.

  2. Additional fragments inside the soft-debounce window reset both timers.

  3. When the timer fires, the pipeline runs with a fresh AbortSignal.

  4. If a new fragment lands after the pipeline has started, the in-flight run is aborted, the buffered fragments are restored to the head of the buffer, and the soft-debounce re-arms - up to the agent’s Max restarts (default 3) consecutive cancel cycles.

  5. After the cap, the next fragments accumulate and flush in a follow-up run.

DEBOUNCED_CHANNELS = {"whatsapp", "telegram"} - every other source skips the coordinator and runs the pipeline immediately. Aborted runs leave zero DB trace: no conversation row, no message row, no memory extraction. The Coordinator also schedules a typing indicator on WhatsApp (Twilio /v2/Indicators/Typing.json) for slow turns; on Telegram it uses native sendChatAction.

STT pipeline

Voice notes are first-class. Each agent selects openai (Whisper), deepgram, aws (Amazon Transcribe), or disabled with its sttProvider setting. When WhatsApp or Telegram receives audio, the engine routes the downloaded bytes to the selected provider before the supervisor sees the transcript; missing credentials and explicit disablement remain different outcomes.

On success, the adapter forwards the transcript plus metadata such as kind, duration, provider, language and latency. The downloaded audio bytes are not attached to the IncomingMessage, and no replayable raw audio is retained.

The agent channel

The agent channel is a virtual, in-process transport with no network hop. When agent A invokes agent B via the supervisor’s delegation path, the call lands on agent B’s AgentChannelAdapter, runs through B’s full pipeline (history, summary, supervisor, tools), and the reply is returned synchronously as a tool result on A’s side.

Each agent-to-agent call carries:

  • callerSlug - which instance initiated the call.
  • callerConversationId - the parent conversation that spawned this turn.
  • depth - recursion counter, capped to prevent infinite loops.
  • parentTraceId - used to stitch LangSmith traces into a single tree.

This is how compositions like “concierge agent delegates billing questions to billing agent” work without any external Slack/Telegram round-trip.

How it works

inbound side outbound side +-----------------------------------+ +-------------------------------+ | Telegram (grammY webhook) | | Telegram.sendMessage() | | Slack (Bolt Events API) | | Slack.chat.postMessage() | | WhatsApp (Twilio webhook) | | WhatsApp / Twilio Programmable | | Web (POST /v1/chat...) | | SSE chunk stream | | Room (Room scheduler tick) | | (configured outbound channel) | | Scheduled (cron tick) | | (configured outbound channel) | | Agent (in-process call) | | (returns to caller) | +-----------------+-----------------+ +---------------+----------------+ | ^ v | +-----------------------+ +-------------------------+ | MessageCoordinator | | OutgoingMessage emit | | (whatsapp+telegram | | | | only - debounce) | +-------------------------+ +-----------+-----------+ ^ | | v | +--------------------------------+ | | Pipeline (handleMessage) | | | - resolve instance config | | | - load history + summary | | | - STT if audio attachment | | | - supervisor.run() +-------------------+ +--------------------------------+

Code reference

  • packages/engine/src/channels/types.ts - ChannelAdapter, MessageChannelType, IncomingMessage, OutgoingMessage, AgentCallMetadata.
  • packages/engine/src/channels/channel-manager.ts - Adapter orchestrator, DEBOUNCED_CHANNELS set.
  • packages/engine/src/channels/message-coordinator.ts - Fragment debouncer with cancel-and-restart.
  • packages/engine/src/channels/adapters/telegram/ - Telegram adapter (grammY).
  • packages/engine/src/channels/adapters/slack/ - Slack adapter (Bolt Events API webhook).
  • packages/engine/src/channels/adapters/whatsapp/ - Twilio Programmable Messaging adapter.
  • packages/engine/src/channels/adapters/agent.adapter.ts - In-process agent-to-agent channel.
  • packages/engine/src/channels/audio-transcription.ts - STT routing.
  • packages/engine/src/stt-gateway/providers/openai-whisper.ts - Default Whisper provider.
  • packages/engine/src/instances/channels.store.ts - Encrypted instance_channels CRUD and the narrow ChannelType union.

See also

Last updated on