Notifications
Configure how ObjectOS routes events into user inboxes — the audience a producer emits to, per-user preferences, and render templates.
This page covers the administrator side of notifications: how events become messages, and the three system-managed objects that stand behind them. For the recipient's view of the inbox — reading and muting notifications — see Notifications under Use.
The notification pipeline
A notification is not a single record. It flows through four stages:
emit() + explicit audience → Notification Event → recipients, filtered by Preference → Inbox Messageemit()— Application code raises an event (an approval submitted, a record shared, an invitation sent) and names its audience. The audience is a required input, and it is what decides who is notified.- Notification Event (
sys_notification) — Eachemit()writes one ingress row. This is the diagnostic log of what happened, before any delivery decision. See Audit Logs for how to read it. - Routing — For each event, ObjectOS expands the audience the producer passed into recipients, checks whether each recipient allows that topic on that channel (Preferences), and renders the message for the recipient's channel and locale (Templates).
- Inbox Message (
sys_inbox_message) — The materialized, per-user result the recipient actually sees in their inbox.
Preferences, Subscriptions, and Templates are the admin/system-side configuration. Inbox Messages are the user-side output.
How routing resolves
For a single event, these are the inputs that decide who gets what:
| Input | Question it answers |
|---|---|
The audience passed to emit() | Who receives this? — the producer names the recipients, and this is the only input that puts anyone on the list |
| Notification Preference | Does this recipient allow it here? — the per-user mute/allow for the topic and channel, applied to every (recipient × channel) pair |
| Notification Template | How is it written? — the subject and body to render for the recipient's channel and locale |
| Notification Subscription | Who declared standing interest? — admin-authored rows, not consulted when an event is routed |
A recipient reaches the inbox when the producer's audience resolves to them and their preference does not mute that topic on that channel. Allow is the default, so a recipient with no preference row still receives the notification, and mandatory topics (security alerts and the like) bypass preferences altogether.
Warning: Subscription rows do not route anything. The subscription-to-recipient expansion is not wired:
emit()requires an explicit audience, and nothing in the pipeline above expands a topic's subscriptions into recipients. Every delivery comes from the audience the producer passed, which makes the Setup Notification Subscriptions grid admin-authored data, not a live routing control — pausing a row (enabled = false) stops nothing, and adding one delivers nothing.
That is a statement about the notification pipeline described on this page. It is not a claim about what a capability or integration running on top of ObjectOS may do with these rows for its own purposes. ADR-0012 and ADR-0030 describe subscription-driven routing as the target state, not today's behaviour: the expansion that would make these rows routing-relevant was not built, and it is not scheduled. If it is ever built, the behaviour described here changes with it.
Preferences and Templates are seeded and maintained by the platform, so those list views mostly show system-generated rows. Subscription rows are the ones an admin authors from Setup, which is what makes that grid read like a routing control.
Notification Preferences
At Setup → Configuration → Notification Preferences (sys_notification_preference).
Per-user × topic × channel toggle (mute/allow), with admin-global defaults. A preference decides whether a given recipient receives a given topic on a given channel.
| Field | Purpose |
|---|---|
user_id | The principal the preference belongs to |
topic | Notification topic (e.g. project.digest) |
channel | Delivery channel (in-app, email, …) |
enabled | Boolean — allow or mute this topic/channel for the user |
digest | Batching cadence for the topic |
quiet_hours | JSON window during which delivery is suppressed |
created_at / updated_at | Audit timestamps |
Entries appear automatically when their source action first runs (for example Submit for Approval, Share, or Invite) — you rarely create preference rows by hand. Use the list to adjust admin defaults or mute a noisy topic.
Notification Subscriptions
At Setup → Configuration → Notification Subscriptions (sys_notification_subscription).
A standing subscription of a principal to a notification topic: the row records that someone declared interest in the topic. It is reference data — no part of the delivery path reads it, so a subscription row neither adds a recipient nor removes one (see How routing resolves).
| Field | Purpose |
|---|---|
topic | The topic being subscribed to |
principal | The subscriber — a selector string; see What to type in principal |
enabled | Boolean — marks the row active or paused. No delivery consults it |
What to type in principal
principal is a single text field, not a picker: the value is a selector
string, and its prefix decides what the value is matched against. The forms
below are what the platform's recipient resolver accepts for any selector
string — the same grammar an emit() audience is written in. A value with no
recognized prefix is read as a bare user id — so typing sales_manager (or
Sales team) addresses a user that does not exist, rather than the group you
meant.
| Value | Who it resolves to | Example |
|---|---|---|
role:name | Everyone whose organization-membership tier is that name, within the organization (sys_member.role) | role:admin |
team:id | Every member of that team, matched on the team id, not its label (sys_team_member.team_id) | team:team_123 |
user:id | That one user, by user id | user:usr_123 |
owner_of:object:id | Whoever owns that one record — the first owner/assignee field the record carries | owner_of:invoice:inv_123 |
| An email address | The user whose email matches it. An address that matches no user is kept verbatim as the recipient id rather than rejected | ada@example.com |
id | No recognized prefix — the whole value is taken as a bare user id | usr_123 |
That list is what the resolver accepts, not a designed contract for
subscription rows: owner_of: and the email form are there because every
selector string goes through the same resolver. Whether either belongs in a
standing subscription is an open question the platform has not settled.
role:here means the organization-membership tier, not a job function. The tiers areowner,admin,delegated_admin, andmember— the standing a user holds in the organization. Job function is a position, and position names are not addressable here:role:sales_managermatches nobody. Role is retired vocabulary everywhere else in ObjectOS (capability is a permission set, distribution is a position); the organization-membership tier is the one exception, and this selector spells it the same way.
Directory lookups behind a selector are deliberately best-effort — an event is never failed because a lookup missed. A selector that matches nothing therefore resolves to zero recipients silently: the row stays valid and enabled, and nothing is reported on it. An unmatched email is the sharper edge — it is kept as the recipient id itself, so a typo becomes a recipient that can never be reached. When a topic reaches no one, check the prefix and the spelling of the value first.
Notification Templates
At Setup → Configuration → Notification Templates (sys_notification_template).
A per (topic × channel × locale) render template. Templates turn an event payload into the subject and body a recipient reads, in their locale and for their channel.
| Field | Purpose |
|---|---|
topic | Topic this template renders |
channel | Channel the rendered output targets |
locale | Locale of this rendering (matched to the recipient) |
version | Template version number |
subject | Rendered title |
body | Message body (markdown) |
format | Output format for the body |
is_active | Boolean — whether this template is used for rendering |
These objects are system-managed: rows are seeded by the platform and the
capabilities you run, so most of your work is editing template content or
toggling is_active, enabled, and preference defaults rather than creating
records from scratch.
Where to go next
| Task | Page |
|---|---|
| Configure the email channel used for delivery | |
| Diagnose events with the ingress log and audit trail | Audit Logs |
| See the recipient's inbox experience | Notifications |
| Set locale defaults that templates match against | Localization |
| Return to the administration overview | Configure |