Skip to content

Channels overview

Ongrid talks to humans through channels. A channel is either:

  • a notification channel — Ongrid pushes alerts to a chat surface; the human reads, but the channel itself never carries a reply back to the agent. This is notify_channels in the DB and the internal/pkg/notify/webhook.go Senders.
  • an IM channel — Ongrid sits on a workspace's chat surface as a bot, reads inbound messages, and runs the same agent reasoning that the web UI runs, streaming the answer back as edits to a placeholder message. This is im_apps in the DB and internal/manager/biz/imbridge/provider/*.

The two are independent. Wiring Telegram alerts (sendMessage out) does not let users chat with the agent on Telegram, and vice versa.

Different table, different credential, same UI

Both surfaces are configured in Settings → Channels in the web UI. Notification channels go to the Notify tab; IM channels go to the IM bridge tab.

Which one do I want?

GoalUseFile
Push fired alerts to a Slack channelSlack incoming webhook (Notify)internal/pkg/notify/webhook.go NewSlackSender
Push fired alerts to Feishu/Lark groupFeishu custom bot (Notify)NewFeishuSender
Push fired alerts to DingTalk groupDingTalk custom bot (Notify)NewDingTalkSender
Push fired alerts to WeCom groupWeCom group bot (Notify)NewWeComSender
Generic JSON POST to your own serviceWebhook (Notify)NewGenericWebhookSender
Telegram alerts (single direction)Telegram bot sendMessage (Notify)NewTelegramSender
Talk to the agent from Slack as a botSlack Socket Mode app (IM bridge)imbridge/provider/slack/
Talk to the agent from TelegramTelegram bot getUpdates (IM bridge)imbridge/provider/telegram/
Talk to the agent from Feishu/LarksuiteFeishu long-connection (IM bridge)imbridge/provider/feishu/

A workspace can run both: e.g. a Slack incoming webhook for alerts plus a separate Slack Socket Mode app for conversation. They share no state.

Notification channels

A notification channel is a stateless Send(ctx, Message) Sender. It is called by the alert pipeline whenever an alert fires (or recovers, or the dampening window expires). All channel types render the same canonical notify.Message shape; only the payload format and the signing protocol differ per provider.

Common fields a Sender receives

go
type Message struct {
    Severity   Severity            // critical | warning | info
    Subject    string              // alert rule name + target
    Body       string              // human-readable detail
    Source     string              // "alert" | "test" | ...
    Labels     map[string]string   // rule, incident_id, device_id, ...
    DedupeKey  string              // pipeline:rule:label-set
    OccurredAt time.Time
}

The Slack sender renders this into the attachments format with a severity-tinted color rail, structured fields (Severity, Source, Rule, Incident, Device, Dedupe), and a ts footer. The Feishu / DingTalk / WeCom senders flatten it to a [SEV] subject\nbody\nsource:…\ndedupe:… text payload because their bot APIs only ship plain text in v1.

Signing models at a glance

ProviderHow it authenticates
SlackThe webhook URL itself is the secret. No extra signing.
FeishuHMAC-SHA256(ts\nsecret), placed in JSON body as sign.
DingTalkHMAC-SHA256(ts\nsecret), placed in URL query as sign.
WeComBot key in URL query. No extra signing.
TelegramBot token in path (/bot<TOKEN>/sendMessage).
WebhookOptional X-Ongrid-Signature: sha256=<HMAC> over body.

The exact implementations live in internal/pkg/notify/webhook.go: signFeishu, signDingTalkURL, signGenericWebhook.

Slack drops the secret field silently

The Slack incoming webhook URL is itself the credential. If you fill the Secret field for a Slack channel, the channel builder drops it before constructing the Sender. This is intentional — Slack's protocol has no separate signing surface for incoming webhooks.

IM channels

An IM channel is a long-running goroutine that:

  1. Connects outbound to the provider (websocket for Feishu/Slack, long poll for Telegram). No inbound ports are opened on the manager.
  2. Receives inbound user messages, filters them through allow_from, and hands the text to bizbridge.Bridge.HandleInbound.
  3. HandleInbound posts a placeholder reply, runs the full agent graph (same one the web UI uses), and streams edits back to the placeholder message id.

The same agent kernel, skills, and persona registry power both the web UI and the IM channels. There is no "IM-specific agent" — the coordinator is the same one you see on /chat.

Inbound is provider-specific, outbound is uniform

Provider differences (Slack envelope_id ack, Telegram update offset, Feishu encrypt_key) live in imbridge/provider/*/stream.go. Once a message is inside bizbridge.HandleInbound everything downstream is provider-agnostic.

default_locale

Both IM and Notify rows carry an optional default_locale. The validator accepts the empty string (auto), en, or zh only — en-US / zh-CN are folded to their primary subtag, and EN-us / a typo'd locale is rejected up front in AppInput.validate.

ValueBehavior
"" (auto)No directive. The LLM mirrors the user's language. Legacy default.
enAdd a Respond in English directive to the system prompt.
zhAdd a 「请用中文回复」 directive to the system prompt.

This is independent from the UI locale (ONGRID_DEFAULT_LOCALE env var or browser language). An IM channel always wins for messages received through it. See the AI output locale feedback for the auto-trigger fallback rule on manager-initiated outputs.

allow_from

The sender allowlist. Required for Telegram and Slack; optional for Feishu/DingTalk. It is parsed exactly once by ParseAllowFrom, shared between the validator and every provider's poll/stream loop so the parse rule has a single definition.

Syntax. Comma, space, newline, tab, semicolon — any combination separates tokens. Order is preserved, duplicates are dropped. The telegram: and tg: prefixes are stripped silently (OpenClaw compatibility).

Per-provider format.

  • Telegram. Numeric user IDs only. Non-numeric tokens and negative values (group chat IDs) are dropped at validate time. At least one ID is required — the bot is publicly discoverable by username, so an empty allowlist would let any Telegram user command a tool-equipped agent. See ADR-031.
  • Slack. User IDs starting with U (or W for Enterprise guests). At least one is required. Find your own by clicking your workspace profile → Copy member ID.
  • Feishu / DingTalk. Optional. These platforms are gated by enterprise-tenant membership; only org members can reach the bot.

Empty allowlist for Telegram or Slack is rejected

The validator returns telegram requires allow_from / slack requires allow_from when the parsed list is empty. There is no "deny by default but allow setup later" mode. The bot is publicly reachable; the operator must consciously open the door.

Failure mode is silent drop. A message from a non-allowlisted sender is logged at WARN with the sender's user_id, then dropped. No reply, no placeholder, no agent run. The bot does not even confirm it exists — the same shape as OpenClaw dmPolicy: allowFrom.

What a fired-alert delivery looks like

text
alert evaluator         → produces notify.Message

notify.Sender (per channel)   ↓ buildBody(msg) → JSON payload
                              ↓ signTarget(endpoint, secret, body) → headers / URL params
                              ↓ POST endpoint
                              ↓ resp.StatusCode in [200, 299] → success

                          chat surface

For deeper detail see the per-channel pages: