This document describes Friday’s messaging channel subsystem: the catalog shown in Settings, the shared channel configuration model, the main-process registry, the unified agent gateway contract, and the currently bundled Telegram runtime.
The source-of-truth files are:
src/shared/channels.tsfor channel ids, shared config types, and runtime status events.src/shared/channel-catalog.tsfor catalog metadata, aliases, docs paths, visibility, setup visibility, markdown capability, and brand icon ids.src/main/channels/registry.tsfor plugin registration, runtime factory registration, adapter lifecycle, inbound dispatch, outbound sends, and status broadcasts.src/main/channels/telegram/plugin.tsandsrc/main/channels/telegram/adapter.tsfor the reference runtime.
Catalog
Friday maintains a channel catalog with stable channel ids, labels,
aliases, UI ordering, markdown support, exposure, and docs paths. Aliases such
as lark, gchat, google-chat, imsg, internet-relay-chat, teams,
nc-talk, nc, twitch-chat, zl, and zlu resolve to their canonical ids.
The channel ids below link to the per-channel markdown files used by the catalog docs path.
Only telegram has a bundled runtime. All other entries are catalog-only
plugins with disabled runtime capabilities. Catalog-only channels can still be
configured in Settings so the UI, stored config, future runtime work, and docs
all share stable ids.
| Channel id | Label | Aliases | Runtime | Official/platform documentation |
|---|---|---|---|---|
clickclack |
ClickClack | none | Catalog-only | No verified vendor-official public API docs found. Existing external channel docs: OpenClaw ClickClack plugin. |
discord |
Discord | none | Catalog-only | Discord Developer Platform, Discord Bots. |
feishu |
Feishu | lark |
Catalog-only | Feishu Open Platform, Lark Developer. |
googlechat |
Google Chat | gchat, google-chat |
Catalog-only | Google Chat developer docs, Google Chat API reference. |
imessage |
iMessage | imsg |
Catalog-only | Apple Messages framework, Apple Business Chat / Messages for Business. |
irc |
IRC | internet-relay-chat |
Catalog-only | IRCv3 specifications, Modern IRC specification. |
line |
LINE | none | Catalog-only | LINE Messaging API, LINE Messaging API reference. |
matrix |
Matrix | none | Catalog-only | Matrix specification, Client-server API. |
mattermost |
Mattermost | none | Catalog-only | Mattermost developer docs, Mattermost integration reference. |
msteams |
Microsoft Teams | teams |
Catalog-only | Microsoft Teams developer platform, Teams Graph API overview. |
nextcloud-talk |
Nextcloud Talk | nc-talk, nc |
Catalog-only | Nextcloud Talk API documentation, Bots and webhooks. |
nostr |
Nostr | none | Catalog-only | Nostr NIPs, NIP-04 encrypted direct message. |
qa-channel |
QA Channel | none | Hidden catalog-only | Internal synthetic test channel. There is no external official documentation. |
qqbot |
QQ Bot | none | Catalog-only | QQ Bot official docs. |
signal |
Signal | none | Catalog-only | Signal does not publish an official bot API for this use case. Official user/device docs: Signal linked devices. |
slack |
Slack | none | Catalog-only | Slack API docs, Slack app manifests. |
synology-chat |
Synology Chat | none | Catalog-only | Synology Chat technical specs, Using Integration in Synology Chat. |
telegram |
Telegram | none | Bundled runtime | Telegram Bot API, grammY documentation. |
tlon |
Tlon | none | Catalog-only | Tlon developer docs, Urbit developer docs. |
twitch |
Twitch | twitch-chat |
Catalog-only | Twitch Chat and Chatbots, Twitch API reference. |
whatsapp |
none | Catalog-only | WhatsApp Cloud API, Cloud API get started. | |
zalo |
Zalo | zl |
Catalog-only | Zalo Official Account API, Zalo API Explorer. |
zalouser |
Zalo Personal | zlu |
Catalog-only | No verified official personal-user automation API docs found. Reuse official Zalo developer docs only for supported Official Account flows: Zalo Official Account API. |
Catalog metadata rules:
docsPathmust point at the matching file underdocs/channels/.docsLabeldefaults to<label> setup.setupVisibleandcatalogVisibledefault tofalseonly for hidden entries.qa-channelis hidden and is for tests, not the Settings catalog.brandIconIdis currently set for Discord, Google Chat, Microsoft Teams, and Slack. Matching light and dark icon assets must exist underresources/icons/brands/<brandIconId>/.markdownCapablerecords provider capability for future runtime work. It does not mean a catalog-only provider can currently send or render messages.
Configuration Model
The stored channel state is a Channel object keyed by ChannelType. Each
channel has an enabled flag, a defaultAccountId, and optional accounts.
Account config uses ChannelAccountProperties:
labelenabledtoken,secret,clientSecretserverUrl,webhookUrlappId,clientId,username,phoneNumber,botUserIdallowFromgroupAllowFromdefaultTargetdmPolicyheartbeat
StoreService creates default channel config for every catalog id. Telegram,
WhatsApp, and Discord have first-class top-level config shapes because they need
common token, allowlist, and account fields. Other channels use the generic
account map.
All channels default to disabled with defaultAccountId: "default". The stored
default direct-message policy is allowlist, so an empty allowFrom list
denies direct messages unless a user explicitly changes dmPolicy to open.
Catalog-only plugins still report their accounts as unconfigured until a runtime
implementation exists.
Treat all token, secret, webhook, and client credential fields as secrets. Do
not log them, add them to docs examples as real values, commit them, or include
them in agent-visible output. The Telegram plugin also advertises
TELEGRAM_BOT_TOKEN and secret assignment paths for future secret-store work.
The Settings detail view writes the same shared account fields for every channel. Phone-number inputs are shown for iMessage, LINE, QQ Bot, Signal, Telegram, WhatsApp, Zalo, and Zalo Personal. Server URL inputs are shown for Discord, Feishu, Google Chat, IRC, Matrix, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, Slack, Synology Chat, Tlon, and Twitch.
Renderer IPC API
The preload layer exposes window.channels:
| Method | IPC channel | Purpose |
|---|---|---|
listCatalog() |
channels:catalog |
Return the shared catalog entries. |
getConfig() |
channels:get-config |
Return the full stored channel config. |
getChannelConfig(type) |
channels:get-channel-config |
Return one channel config by id. |
saveChannelConfig(type, config) |
channels:save-channel-config |
Normalize and persist one channel config. Telegram saves also reconfigure the registry. |
getStatus(type?) |
channels:get-status |
Return cached runtime status. Defaults to Telegram when omitted. |
getTelegramConfig() |
channels:telegram:get-config |
Compatibility helper for Telegram config. |
saveTelegramConfig(config) |
channels:telegram:save-config |
Compatibility helper that normalizes and persists Telegram config. |
getTelegramStatus() |
channels:telegram:get-status |
Compatibility helper for Telegram status. |
startTelegram() |
channels:telegram:start |
Save current config and start Telegram polling. |
stopTelegram() |
channels:telegram:stop |
Stop Telegram polling. |
restartTelegram() |
channels:telegram:restart |
Stop and start Telegram polling with current config. |
onStatusChanged(callback) |
channels:status-changed |
Subscribe to runtime status broadcasts. |
ChannelsIpc validates channel ids with normalizeChannelId() before reading
or writing config. Unsupported ids throw an IPC error.
The renderer currently exposes runtime controls only for Telegram. Generic
startChannel(), stopChannel(), and restartChannel() methods exist on
ChannelRegistry for tests and future host code, but they are not renderer IPC
APIs yet.
Registry And Plugin Lifecycle
ChannelRegistry owns registered plugins, runtime factories, active adapters,
channel configs, and the status cache.
Startup behavior:
- Register catalog-only plugins for every catalog entry.
- Register the Telegram runtime plugin, overriding the catalog-only Telegram entry.
- Register the lazy Telegram runtime factory, which imports the adapter only when Telegram is started.
- Register any injected runtime factories from tests or future host code.
Runtime start behavior:
- Resolve and validate the canonical channel id.
- Persist or reuse the channel config.
- Resolve the plugin and default account.
- Skip start if the account is disabled, unconfigured, or the channel has no runtime factory.
- Create the adapter, subscribe to adapter status and message events, and call
adapter.start().
Status events are cached, emitted on the main event bus as channel:status,
and broadcast to the renderer as channels:status-changed.
Unified Gateway Contract
ChannelRegistry is Friday’s only agent-facing message gateway for channel
traffic. Provider runtimes may use webhooks, polling, WebSockets, local device
bridges, or hosted APIs, but the agent must only receive and send the shared
channel message shapes.
Every runtime provider must implement the same boundary:
| Direction | Provider responsibility | Shared format |
|---|---|---|
| Message in | Receive provider events, validate provider authentication, deduplicate platform message ids, preserve provider facts in provenance, and emit the raw channel message through ChannelAdapter.onMessage(). |
ChannelInboundMessage |
| Agent dispatch | Let runChannelTurn() resolve the account, normalize the inbound message, apply security and admission policy, record diagnostics, call AgentService.send(), and resolve the reply route. |
ChannelNormalizedInboundMessage |
| Message out | Accept sends only through ChannelRegistry.send(), map to, threadId, and replyToMessageId to the provider API, and return a delivery result. |
ChannelOutboundMessage to ChannelMessageReceipt |
Do not add provider-specific agent dispatch paths, provider-specific renderer
send IPC, or direct AgentService calls inside a provider runtime.
Provider-specific fields belong in config, target parsing, provenance, and
receipts. Catalog-only providers must keep runtime capabilities disabled until
they can satisfy this gateway contract.
Inbound Flow
For runtime channels, inbound messages follow this path:
- The platform adapter receives a platform event and emits a
ChannelInboundMessage. This is the only provider-to-agent message-in entry point. ChannelRegistry.handleMessage()resolves the plugin, account id, and saved config.runChannelTurn()resolves the configured account.- The plugin normalizes the raw message to a
ChannelNormalizedInboundMessage. resolveIngressAdmission()evaluates the plugin security adapter.- Allowed messages are recorded and dispatched to
AgentService.send(). - The registry resolves the reply target and sends the agent reply through the channel adapter.
Admission outcomes:
dispatch: send to the agent.handled: accepted by the channel layer but not dispatched, for example a pairing-required direct message.observeOnly: observed but not dispatched, for example a mention miss.drop: rejected before agent dispatch.
Ingress diagnostics intentionally include channel, account, chat type, and reason only. Raw sender ids and route ids are not copied into diagnostics.
Outbound Flow
Outbound sends use ChannelOutboundMessage:
await channelRegistry.send({
type: 'telegram',
accountId: 'default',
to: '-1001234567890',
threadId: '42',
replyToMessageId: '101',
text: 'Hello from Friday',
idempotencyKey: 'example:reply',
});
The registry requires a running adapter for the target channel. Catalog-only channels reject sends because they have no active runtime adapter.
Durable send helpers return ChannelMessageReceipt with:
sentwhen every part was delivered.partialwhen some parts were delivered before a failure.failedwhen nothing was delivered.
Telegram Runtime
Telegram is implemented with grammy.
Current behavior:
- Uses long polling with
drop_pending_updates: true. - Emits
connecting,connected,disconnected, anderrorstatus updates. - Runs a
getMe()health check every 60 seconds. - Reconnects with exponential backoff from 2 seconds up to 60 seconds.
- Receives plain text messages only.
- Ignores slash commands before agent dispatch.
- Deduplicates inbound messages by Telegram-derived idempotency key.
- Infers chat type from Telegram chat type and forum topic id.
- Builds session keys as
telegram:<accountId>:<chatId>[:<threadId>]. - Sends replies to the original chat, thread, and message id when present.
- Splits outbound text into Telegram’s 4096-character message limit.
Telegram target strings can be explicit:
telegram:<chatId>
telegram:<accountId>/<chatId>
telegram:<accountId>/<chatId>#<threadId>
Negative Telegram ids generally represent groups or supergroups. Thread ids map to Telegram forum topic message thread ids.
Adding A Runtime Channel
Use the existing Telegram plugin as the template.
- Add or verify the id in
CHANNEL_PROVIDER_IDS. - Add catalog metadata in
CHANNEL_CATALOG_INPUT, including aliases, markdown capability, exposure, docs path defaults, andbrandIconIdwhen a local brand icon should be used. - Define a shared config shape only if the generic account shape is not enough.
- Implement a
ChannelPluginwith config, setup, security, messaging, threading, and doctor adapters. - Implement a runtime
ChannelAdapteras the provider gateway withstart(),stop(),send(),onMessage(), andonStatus(). It must convert provider message-in events toChannelInboundMessageand convertChannelOutboundMessagesends to provider API calls that returnChannelMessageReceipt. - Register the plugin and runtime factory in
ChannelRegistry. - Add IPC helpers only when generic
saveChannelConfig()andgetStatus()are not enough. - Update Settings UI only for fields, runtime actions, or status behavior that need channel-specific controls.
- Add tests for catalog normalization, config normalization, ingress admission, message normalization, durable sends, registry start/stop/send behavior, and any target parsing rules.
- Add or update the per-channel docs page, official docs links in the catalog
table above, and brand icon assets/tests if
brandIconIdis set.
Do not bypass ChannelRegistry, ChannelRegistry.send(), or
runChannelTurn() for agent message-in or message-out. That path keeps account
resolution, security policy, diagnostics, and reply routing consistent across
runtimes.