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→erroris 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 —
estatewith a node-path selector in the authorisation rule grammar (/projects/acme,/projects/*/environments/prod,**any depth; a scope covers its whole subtree), orplatformfor 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 aswarn:licence:), state predicates (started,succeeded,failed,cancelled,waiting_on_gate), a severity floor (info<warn<error), and attribute predicates — a glob onaction_path, so*start*coversweb: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,slackorteams). Deliveries are signed over a timestamped body withX-OpsChain-SignatureandX-OpsChain-Timestamp— the same idiom the inbound lane verifies (lesson 5.3).slackandteams— 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,hourlyordaily. 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) oroff. It governs only your own-run notifications — approvals waiting on you are actionable, not informational, and keep flowing even atoff; 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) withopschain admin notifications connectors; users point routes at them; connector deliveries retry, then dead-letter asfailed. - 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).