Clapilot-Agent Channels

Native channel pipeline for Telegram, Slack, WhatsApp, Signal, and iMessage.

Generated explanatory diagrams

Channel architecture explained visually

These raster diagrams are generated documentation assets and are paired with exact captions below so the page is readable even when a tiny in-image label is hard to inspect.

Approval pipeline
Approval pipeline

Shows provider ingress, parsing, event deduplication, thread resolution, approval gating, native execution, and reply delivery.

Thread to session mapping
Thread to session mapping

Explains how DMs, topics, Slack threads, and WhatsApp groups can remain channel-local or map onto linked users and team rooms.

Transport strategy
Transport strategy

Compares Telegram polling and streaming edits, Slack webhooks, and WhatsApp Web QR/auth-state transport before all paths merge into native channels.

Channels are how people reach the same Clapilot agent from outside the web app. The important idea is that external messages are not trusted just because they arrive. Every provider event is parsed, deduplicated, checked against the approval model, mapped to a session, and only then allowed to trigger a runtime run.

Use this page when Telegram, Slack, WhatsApp, Signal, or iMessage behavior differs from web chat, or when you need to understand why an external thread did or did not receive a reply.

The native runtime supports five channel types in agent_channel_configs:

  • telegram
  • slack
  • whatsapp
  • signal
  • imessage

Main implementation:

  • services/clapilot-agent/src/channels/index.mjs
  • services/clapilot-agent/src/channels/whatsapp-web.mjs
  • services/clapilot-agent/src/channels/signal-imessage.mjs

Shared channel pipeline

All inbound channels follow the same core flow:

  1. load channel config from agent_channel_configs
  2. parse the provider-specific inbound payload
  3. deduplicate using the external event id recorded in agent_events
  4. resolve or create a thread record in agent_channel_threads
  5. upsert an approval record in agent_channel_approvals
  6. block unapproved senders/groups or blocked DMs
  7. optionally ignore group/channel messages that do not mention Clapilot when the per-channel mention-only setting is enabled
  8. run the conversation through Clapilot delegation or the native session engine
  9. send the reply back through the provider API

When a repo/coding request is detected in an approved channel thread, the runtime can now route into the native Agent Orchestrator session broker instead of the normal native chat loop:

  • existing interactive session for that thread: continue it
  • no session yet but the message looks like repo/coding work: create one and bind it to the thread
  • normal business/document conversation: stay on the standard native session path

Thread identity is normalized to one of two session-key modes:

  • fallback channel-local key: channel:<channel>:<threadKey>
  • mapped canonical key: the same agent:main:openai-user:<session_user> key family used by web chat and team chat

Approved Telegram/Slack/WhatsApp/Signal/iMessage threads can therefore be mapped onto existing Clapilot session identities instead of always living in a channel-local memory silo.

For Telegram forum/group topics, threadKey is now topic-aware:

  • direct messages: <chatId>
  • groups without topic id: <chatId>:root
  • forum/topic messages: <chatId>:<message_thread_id>

Approval scope stays anchored to the parent Telegram chat/group id, but session/memory scope is split per topic.

Security and approval model

The runtime never auto-replies to unknown external subjects.

Approval data is stored in agent_channel_approvals with:

  • channel_type
  • subject_type dm or group
  • subject_key
  • thread_key
  • sender_key
  • status pending, approved, denied

Behavior:

  • new DM or group arrival creates or refreshes an approval record
  • until approved, the runtime logs the event but does not answer
  • direct messages can also be globally blocked per channel with allow_direct_messages = false
  • approved groups/channels can be switched to mention-only mode per channel with settings.mention_only = true; DMs are unaffected
  • approved DMs should be linked to a specific Clapilot user; the channel thread then reuses that user's main-chat session key
  • approved groups can be mapped to any non-archived public/private team-chat channel or group room; the room picker offers every mappable room available to the admin, with clapilot-members first, and the channel thread then reuses the selected room's team-chat session key
  • approved mappings are mutable; admins can later change the linked user / room or move the entry back to pending

Outbound sends from normal agent runs are also constrained to approved recipients. Recipient matching uses stored keys and metadata such as usernames, display labels, Slack channel ids, or WhatsApp phone ids.

When an outbound send is delivered through an approval, the runtime also records the sent assistant text into the mapped conversation session from approval metadata (session_key, or the mapped group room when only group_room_id is available). Sent Telegram media is recorded as workspace file references in that same internal context entry. Sends into the run's own session are not double-recorded because the run output already captures that assistant turn.

Successful /internal/channels/send responses include group_room_id for the Team Chat room mapped by the matched approval and mirrored_room_id only when the outbound message was actually persisted in that room. Callers, including automation deduplication, must use mirrored_room_id rather than assuming that a configured mapping proves the mirror succeeded.

Team-Chat-Spiegelung

Approved Telegram, WhatsApp, Slack, Signal, and iMessage groups can opt into one-way mirroring from the external channel into any mapped public/private Team Chat channel or group room. The approval room picker offers all non-archived mappable rooms available to the admin. Mirroring is enabled only when the approval row has an explicit, non-empty metadata.group_room_id; the implicit clapilot-members session fallback does not enable it.

  • inbound participant messages appear as user messages in the mapped room, including non-mention chatter when mention-only mode suppresses an agent run
  • a sender is attributed to a real Clapilot user when an approved DM mapping for the same channel sender contains metadata.linked_user_id; otherwise the message uses a synthetic channel display name based on the external name, username, or sender key
  • delivered agent replies are mirrored for normal auto-replies, channel_send_message tool calls, and automation sends
  • inbound Telegram and WhatsApp photos/documents plus best-effort Signal and iMessage images are persisted under .clapilot/channel-media/<channel>/<yyyy-mm>/ in the shared workspace and appear as real Team Chat attachments; downloads and payloads are limited to 20 MB per attachment
  • inbound files that cannot be persisted or exceed the cap retain a compact 📎 text placeholder; outbound workspace media appears as the same real Team Chat attachment shape without a duplicate placeholder
  • the mapped room's built-in main-agent membership is authoritative for mirrored inbound messages: removed means no default channel reply, mention_only requires an explicit main-agent @ mention, and all_messages keeps the default reply path active unless the message explicitly targets an invited specialist; an explicit specialist target suppresses the default main-agent run unless the main agent is also explicitly mentioned
  • voice notes remain transcript-only in Team Chat and are not copied as audio attachments
  • mirrored user and agent messages show the small channel brand icon next to the sender name in the Team Chat UI, with a localized “sent via” tooltip

The reverse direction is separately opt-in per approved group mapping through metadata.mirror_to_channel = true in the approval room picker. When enabled, Team Chat user messages are posted to the mapped external group as [Name via Clapilot] followed by a newline and the message text; agent replies are posted as plain bot messages without a prefix. Telegram receives real image/document attachments from Team Chat: base64 attachments are materialized under .clapilot/channel-media/outbound/<yyyy-mm>/ in the shared workspace, workspace-backed attachments reuse their relative path, and each file is limited to 20 MB. For reverse sends that contain text and media, Telegram receives the text first and then the files. WhatsApp, Slack, Signal, and iMessage receive 📎 <name> placeholders instead of outbound media, and Telegram uses the same placeholder for attachments that cannot be materialized or exceed the cap.

Rows whose message_meta.channelOrigin is set are never forwarded back to the external group. Reverse sends also use messageOrigin: "team_chat_forward", which tells sendOutbound to skip its normal external-channel-to-Team-Chat display copy. Together these guards prevent echo loops and duplicate Team Chat rows while leaving provider delivery and mapped-session behavior unchanged.

Automations should target either the Team Chat room or the channel approval when reverse mirroring is enabled. If an automation explicitly targets both, the external group can receive two intentional deliveries: the direct channel delivery and the reverse-mirrored Team Chat delivery.

Telegram

Telegram is the most complete native channel path today.

Default mode is polling:

  • the runtime uses getUpdates
  • it clears any webhook for the active token when polling is enabled
  • it maintains a watchdog and aborts stalled polls

Optional mode is webhook:

  • Telegram posts to Clapilot’s public route
  • the Next.js app forwards internally to clapilot-agent

Streaming reply behavior is native. The accepted settings.streaming values are partial, block, and off (off is normalized to the internal value none; both spellings disable preview streaming):

  • partial Telegram uses one visible reply message and edits that same message in place while deltas arrive Telegram photo/document attachments are downloaded by clapilot-agent and normalized into native multimodal message parts before the run
  • block compatibility preview mode
  • off no native preview streaming

Telegram-specific settings live in agent_channel_configs.settings, for example:

{
  "transport": "polling",
  "streaming": "partial"
}

The runtime also sends typing indicators and falls back from unsupported draft APIs to normal sendMessage plus editMessageText.

Telegram + interactive orchestrator details:

  • outbound replies still target the original message_thread_id
  • interactive coding sessions are bound by the Clapilot-owned topic-aware threadKey, not by Telegram-native ACP support
  • session bindings are persisted in agent_external_sessions and detailed lifecycle/tool/terminal events are audited in agent_events
  • auto-routing into the interactive orchestrator is intentionally conservative: repo/coding work starts sessions, and existing sessions only continue on coding-like follow-up messages, not on meta questions about ACP/Telegram/session capabilities
  • if a thread already has an active coding session and the user asks a meta status question about that session, the channel runtime answers from the real session state instead of forwarding the question into the coding agent or guessing from generic model knowledge
  • retry-style follow-ups in a thread with an existing orchestrator binding, such as versuchs nochmal or try again, are treated as orchestrator continuations; if the previous session was already closed, the runtime starts a fresh replacement session for the same thread key

Detailed Telegram page:

WhatsApp

WhatsApp Web is connected through the backend-owned Baileys QR session. Inbound push-to-talk voice notes and regular audio messages are downloaded by clapilot-agent, persisted under .clapilot/channel-media/whatsapp/<year-month>/, and transcribed through the configured Clapilot STT runtime before the normal channel run starts. The transcript becomes the effective user message; raw audio is not sent to the model as a generic file.

When the approved WhatsApp group is mapped to a Team Chat room, the same workspace audio file is mirrored as a playable audio attachment together with the transcript. If download, persistence, or STT fails, the runtime records whatsapp_audio_transcription_failed, keeps a successfully persisted audio attachment visible in the mapped room, and returns a concrete channel error instead of passing <media:audio> to the agent.

Slack

Slack inbound handling is webhook-driven.

Current behavior:

  • handles url_verification
  • parses event.channel_type
  • distinguishes DM vs channel/thread
  • builds thread identity as <channel>:<thread_ts|ts>
  • sends replies with chat.postMessage

Slack-specific implementation details:

  • thread continuity is preserved through thread_ts
  • DM approval is per Slack user or DM thread
  • channel approval is per Slack channel id
  • mention-only mode treats Slack app_mention events as explicit mentions

The runtime currently uses the saved bot token from agent_channel_configs.credential_*.

Signal

Signal uses an external signal-cli-rest-api service that the administrator operates separately. Clapilot does not register or link Signal accounts itself.

Prerequisites:

  • run bbernhard/signal-cli-rest-api in a mode where GET /v1/receive/{number} is available
  • link the account in the external service, for example through {baseUrl}/v1/qrcodelink?device_name=Clapilot
  • keep the service reachable from the clapilot-agent runtime

Channel settings:

{
  "baseUrl": "http://192.168.1.10:8080",
  "number": "+49123456789",
  "pollIntervalMs": 2000
}

Runtime behavior:

  • ClapilotAICore long-polls GET {baseUrl}/v1/receive/{number}?timeout=20
  • polling uses a hard timeout, watchdog, and bounded exponential error backoff
  • receipts, typing events, sync envelopes, and empty data messages are ignored
  • DMs use the source number/UUID as their thread and reply key
  • groups retain the exact inbound dataMessage.groupInfo.groupId, including a group. prefix when supplied, so replies round-trip through POST {baseUrl}/v2/send
  • image attachments are fetched best-effort from GET {baseUrl}/v1/attachments/{id} and become multimodal image parts when available

Every new Signal DM or group must pass the same channel approval flow before it can trigger a reply. Direct messages also respect allow_direct_messages; approved groups respect mention-only mode. Outbound messages are text-only. Signal registration, outbound media, reactions, receipts, and typing indicators are intentionally not managed by Clapilot.

iMessage via BlueBubbles

iMessage uses an external BlueBubbles Server running on a Mac. Clapilot receives BlueBubbles webhooks and sends replies through its REST API; it does not automate BlueBubbles installation or Messages.app setup.

Prerequisites:

  • install and configure BlueBubbles Server on a Mac signed into Messages
  • make the server reachable from clapilot-agent
  • create a BlueBubbles webhook for new-message pointing at https://<clapilot>/api/agent-runtime/channels/imessage/inbound?password=<BlueBubbles-password>

Channel configuration uses settings.serverUrl; the BlueBubbles password is stored in Clapilot's encrypted channel credential columns rather than plaintext settings JSON:

{
  "serverUrl": "https://bluebubbles.example.com"
}

Webhook authentication is mandatory. BlueBubbles versions and clients may name the query parameter password or guid; Clapilot accepts either name and requires its value to match the saved encrypted password. Missing or mismatched values receive 403 and are never parsed as messages.

Runtime behavior:

  • only type: "new-message" events are accepted
  • messages with data.isFromMe === true, missing chat GUIDs, or no text/image are ignored
  • data.guid is the deduplication key
  • data.chats[0].guid is retained as the DM/group thread and outbound chatGuid
  • the sender label comes from data.handle.address
  • image attachments are fetched best-effort from GET {serverUrl}/api/v1/attachment/{guid}/download?password=...
  • replies first call POST {serverUrl}/api/v1/message/text?password=... with method: "private-api"; a failed Private API request is retried once with method: "apple-script"

Every new iMessage DM or group must be approved before Clapilot replies. Direct messages also respect allow_direct_messages; approved groups respect mention-only mode. Outbound messages are text-only. Reactions, delivery/read receipts, typing, outbound media, and BlueBubbles setup automation are not included.

WhatsApp

The runtime currently supports a backend-owned WhatsApp Web session based on the Baileys client.

Important points:

  • pairing is done by QR from the ClapilotAICore settings UI
  • auth state is persisted under .clapilotaicore/channels/whatsapp/<account>
  • the runtime maintains the connection and reconnect loop itself
  • outbound sends go through the active Web session
  • mention-only mode compares WhatsApp group mention JIDs against the linked account JID

Runtime auth/status endpoints:

  • GET /internal/channels/whatsapp/auth
  • POST /internal/channels/whatsapp/auth

Supported auth actions:

  • start
  • wait
  • logout

The codebase still contains a generic webhook parser for WhatsApp-shaped payloads in channels/index.mjs, but the actively managed native path is the backend-owned WhatsApp Web module in whatsapp-web.mjs.

If you still expose Clapilot's public WhatsApp inbound route for a Meta-style webhook compatibility path, the app now answers GET /api/agent-runtime/channels/whatsapp/inbound verification requests directly. Configure the verify token in the persisted channel settings JSON under webhook_verify_token, webhookVerifyToken, verify_token, or verifyToken.

Transport strategy

Channel execution can take two reply paths:

  1. preferred: delegate into Clapilot through /api/agent-runtime/channel-response
  2. fallback: run directly in the native session engine

Why that split exists:

  • Clapilot delegation reuses the broader app/business action layer
  • native fallback keeps the channel operational if that bridge is unavailable

Telegram streaming is an exception: when native streaming is enabled, the reply path is handled directly inside clapilot-agent so preview updates can be emitted during generation.

Configuration model

Per-channel config lives in agent_channel_configs:

  • enabled
  • allow_direct_messages
  • inbound_path
  • encrypted credential
  • provider-specific settings JSON, including mention_only = true for group/channel mention-only replies

Seeded default rows are created for all five supported channels.

Admin UI responsibilities:

  • save credentials
  • toggle enablement and DM policy
  • toggle group/channel mention-only behavior
  • review and approve pending contacts/threads
  • manage WhatsApp QR pairing

Data written by the runtime

Channel events and metadata are stored across:

  • agent_channel_configs
  • agent_channel_approvals
  • agent_channel_threads
  • agent_events
  • agent_runs
  • agent_session_state

This separation matters:

  • config is static admin state
  • approvals are security decisions
  • threads bind external thread ids to internal session_key
  • runs/events provide diagnostics and auditability

Operational notes

  • Telegram polling is the default and requires no public webhook.
  • Signal polling requires network access from ClapilotAICore to the external signal-cli-rest-api service.
  • Slack, iMessage, and other webhook-style ingress require a public Clapilot endpoint because the app forwards into the internal service.
  • WhatsApp Web is stateful and depends on persisted auth files inside .clapilotaicore.

Related docs: