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 hands each accepted delivery to its channel, which renders it. What a channel renders from differs by channel: email and SMS render Notification Templates; the in-app inbox does not (see What each channel renders).
- 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 rendered on the email and SMS channels. The in-app inbox does not read these rows; see What each channel renders |
| 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, read by the email and SMS channels: a template turns an event payload into the subject and body those channels send. The in-app inbox does not read these rows — see What each channel renders.
| Field | Purpose |
|---|---|
topic | Topic this template renders |
channel | Channel the rendered output targets |
locale | Locale of this rendering — the notification's own, not the recipient's |
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 |
The locale is matched on the notification, not the recipient: a delivery
renders with payload.locale when the producer sets one, else the deployment
default. Nothing resolves a locale from the person receiving it, so one emit
renders in a single locale for every recipient it reaches — authoring es and
en rows for a topic gives the producer two renderings to choose between, not
two recipients reading in their own language.
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.
What each channel renders
Rendering happens per channel, and only two channels read the rows above:
| Channel | Renders from |
|---|---|
The Notification Template matching (topic, email, locale). With no matching active row, the notification's own title and body are sent as-is | |
| SMS | The Notification Template matching (topic, sms, locale), with the same fallback |
| In-app inbox | Not a Notification Template. The inbox row is written from the notification's own title and body — the topic, when the producer set no title. When the notification names an email template instead, the inbox renders that one: a sys_email_template row resolved by (name, locale) through the email service, which Email documents. Without an email service able to render it, that delivery fails rather than falling back |
Warning:
inboxis the default channel — a notification that names no channels is delivered there and nowhere else. So the delivery an admin is most likely looking at is the one that never reads a Notification Template: editing a template row here changes what email and SMS send, and changes nothing about the in-app message. This page describes the wiring as it stands; it does not forecast an inbox seam to these rows.
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 |