Skip to main content
Version: v5.7.0

5.5 Notifications: watches, channels, the bell

Included from Basic tier

After this you can: create a watch on what you care about, deliver it to a channel, read the bell's feed and its history, and tune the noise.

Before you start — tick each one:

  • Lesson 5.1 — The record: events and chains (opschain learn 5.1)

In the GUI: the bell beside the profile menu carries your unread count and opens the in-app feed (History, Mark all read); the Notification settings page behind it holds your watches, and the sidebar's Inbox wears the red bubble for what waits on you (an approval notification opens Inbox → Approvals); the feed groups one run's notifications under one thread.

Lesson 5.1 taught you to go and read the record. Notifications are the record coming to you: a server-side engine reads the same events stream, matches each event against what people have declared they care about, checks that they may read it, and delivers one row per (user, event) — to the bell, and optionally to a chat channel or a webhook. By the end you will know what reaches you with no setup, how to add a watch, how an administrator opens a channel, and which knobs keep it quiet.

1. What reaches you with no setup​

Before anyone subscribes to anything, two things already notify every user, in-app:

  • Your own activity — a change or workflow run you created reaching a terminal state. One updating row per run: a new row in the same thread supersedes the older one, so a run that went running → error is one bell entry, not three.
  • Approvals waiting on you — a wait gate naming you, or a subject that entered the approval funnel with your name (or a group of yours) on a line still unsatisfied. The row reads Change awaiting your approval: <target> · <action> — requested by <requester> and links straight to the approvals inbox; when the ceremony closes, Change approval <outcome>: … lands in the same thread and supersedes the call to action.

Run a change of your own (lesson 3.1) and watch the bell: its badge counts your unread rows, and the row that appears carries matched_by: my_activity. Summaries are rendered from an allowlisted field set only — event type, node path, action path, actor — never from event payloads, so a resolved secret or a person's data cannot enter a notification; the deep link carries you to the subject, where your read authorisation is checked again.

2. Watch what you care about​

A watch (a subscription) is Scope × Conditions × Route × Controls, and you manage your own on the Notification settings page ("Watches") or through NotificationService on the v2 API (opschain docs api-v2) — strictly your own rows; no caller, superuser included, reaches another user's watches. The CLI carries the administrator's side of notifications (section 3 and section 7), not the personal one.

  • Scope — estate with a node-path selector in the authorisation rule grammar (/projects/acme, /projects/*/environments/prod, ** any depth; a scope covers its whole subtree), or platform for the admin-object happenings (runners, agents, licence, maintenance, posture — the platform scope needs the admin grant).
  • Conditions — object kinds (change, workflow_run, …), event kinds (exact types, or a prefix with a trailing : such as warn:licence:), state predicates (started, succeeded, failed, cancelled, waiting_on_gate), a severity floor (info < warn < error), and attribute predicates — a glob on action_path, so *start* covers web:start:run. Every predicate must match.
  • Route — absent means the always-on in-app bell; otherwise one of your delivery routes (section 3).
  • Controls — enabled, muted, snoozed-until, and the cadence (section 5).

Creating an estate watch needs read on the scope's literal prefix path — a scope you cannot read answers not-found, lesson 7.1's rule — and the engine re-checks your read authorisation at match time and again at dispatch: a revocation in between records a suppressed row instead of a delivery, so losing access stops the notifications. A group can hold team watches too — matched against the group's own grants, never a union of its members', delivered once per event to a team channel — shown read-only on your settings page under "Team watches".

Every write is an ordinary audit event (api:notification_subscriptions:create|update|destroy|mute|snooze), names and ids only.

3. Channels: connectors and routes​

A notification leaves the bell through a route, and a route points at a connector — an instance-level object an administrator configures, never a URL a user pastes. Three kinds:

  • webhook — an allowlisted set of HTTPS URL prefixes and an HMAC signing secret (write-only); each route picks a target URL inside the allowlist and a payload template (generic, slack or teams). Deliveries are signed over a timestamped body with X-OpsChain-Signature and X-OpsChain-Timestamp — the same idiom the inbound lane verifies (lesson 5.3).
  • slack and teams — the connector stores the incoming-webhook URL itself (a capability secret, write-only) and the kind implies the payload shape, so these carry no allowlist, secret or template.

The administrator creates and proves one; the secret rides an environment variable, never the command line:

OPS_SLACK_URL=https://hooks.slack.com/services/… \
opschain admin notifications connectors create --name ops-slack --kind slack \
--webhook-url-env OPS_SLACK_URL
connector "ops-slack" created (01a14801-…-example) — kind slack
opschain admin notifications connectors test 01a14801-…-example
test delivered — HTTP 200
opschain admin notifications connectors list
ID NAME KIND ALLOWLIST SECRET ENABLED
01a14801-…-example ops-slack slack - set true

A webhook connector takes --kind webhook --allow-url https://… --signing-secret-env <var> instead, and test --url … --template … posts inside its allowlist. The HTTP posture is deliberately hostile to request forgery: HTTPS only, redirects never followed, loopback and link-local targets refused unless allowlisted literally. Deleting a connector a route still points at is refused by name.

Now any user can add a delivery route on the Notification settings page ("Delivery routes") — pick the enabled connector from the catalogue (the only thing a non-admin sees of it) and point a watch at the route. Connector-routed rows land queued and a cluster-wide sweeper delivers them: up to 5 attempts on a backoff ladder (30s/2m/8m/30m), then the row dead-letters as failed with the reason in delivery_error. The in-app row stays readable whatever its egress fate, and an unsigned webhook delivery is never sent.

4. The bell, the feed, the history​

The bell is live: the server streams fresh rows and unread-count ticks to it (WatchNotifications on the v2 API), so the badge is right before you click. Opening it shows the unread feed newest first; a row's link takes you to the subject. Mark all read clears the badge, and History keeps what you have read — a row you acted on yesterday is still there until feed retention archives it (section 7). Reading and archiving are stamped as api:notifications:read / archive events, counts only, never summaries.

5. Noise controls​

  • Cadence. Every row is stamped with its resolved cadence — default, realtime, hourly or daily. The default posture is realtime for failures, errors and approvals-waiting, a daily digest for everything else; an explicit value on a route overrides it. The in-app row always lands realtime — cadence governs when a connector-routed copy leaves, held until its digest window closes.
  • Thread grouping. One updating row per change or workflow run; the superseded rows stay as evidence and in the thread history but leave the feed, the badge and the stream.
  • Flood collapse. Five failures within five minutes on one thread collapse to one row (N failures on <path>); a flood across several threads under one watch collapses to one row naming the watch's scope as the blast radius.
  • Mute, snooze, disable — per watch, on the settings page.
  • My activity. The pinned control at the top of the settings page: all (the default), failures_only (only your failed runs notify) or off. It governs only your own-run notifications — approvals waiting on you are actionable, not informational, and keep flowing even at off; your explicit watches are untouched.

6. Approvals waiting on you, and the badge​

When a subject enters the approval funnel — a governed change or workflow run held pending_approval, a trust raise, a release promotion, a transport grant — every user the approvals inbox would show it waiting on you to is notified in-app, realtime, authorised by the inbox's own rule (read on the checklist target). The Inbox entry in the sidebar carries the matching count in its red bubble, so the bell and the sidebar agree about what is yours to decide. Approving, rejecting and the rest are lesson 10.1 (opschain learn governance-approvals).

7. What you cannot silence, and the receipts​

A mandate is a watch an administrator attaches to a group whose members cannot delete, mute, snooze or disable it — the member verbs refuse with the typed MANDATED_SUBSCRIPTION reason, and the settings page labels such rows "Mandated by your organisation". Every dispatched row doubles as the delivery evidence: who was told what, when, on which channel, with what outcome (queued, delivered, failed, suppressed), metadata only:

opschain admin notifications deliveries --username alice --status failed
RECIPIENT CHANNEL STATUS SEVERITY EVENT CREATED AT SUMMARY
alice slack failed error api:changes:error 2026-10-12T09:52:10Z web:deploy failed on /projects/acme/environments/prod/assets/web

A failed delivery under a mandate is a finding: the mandate said this person must know, and the channel did not deliver. Two retention clocks sit on the one record — feed retention archives a row out of every feed view, evidence retention deletes it — derived from the licence tier (Basic fixed at 30/90 days; Enterprise 90/400 by default and tunable):

opschain admin notifications retention show
feed 30 day(s), evidence 90 day(s) — tier basic, tunable false

Mandate administration, the evidence query and retention tuning are the packs:security-governance surfaces; enforcement of existing mandates and the reaper never consult the licence. Hands-on, with the ledger closing the loop: lesson 11.4 (opschain learn governance-notifications). The component map is opschain docs notifications; the resource dictionary with the full selector grammar is opschain docs resources/notification.

Check yourself​

  • Run a change of your own and open the bell: which arm matched the row (matched_by), what does the badge count, and where do you find that row after you have marked it read? Then: which setting would stop your successful runs notifying you while keeping approvals flowing?

What you learned​

  • Without any setup, your own runs' terminal states and approvals waiting on you reach the bell, one updating row per run.
  • A watch is scope × conditions × route × controls, created on the Notification settings page or the v2 API, authorised at match and at dispatch; groups hold team watches.
  • Administrators create and test connectors (webhook, slack, teams) with opschain admin notifications connectors; users point routes at them; connector deliveries retry, then dead-letter as failed.
  • Cadence, thread grouping, flood collapse, mute/snooze and the My activity preference keep the feed quiet; approvals never go quiet.
  • Mandates cannot be silenced, every row is delivery evidence (admin notifications deliveries), and two retention clocks govern how long it stays.

Next: lesson 5.6 — Maintenance windows (opschain learn 5.6), or back to your track (opschain learn).