Notifications

Assignments, approvals, expirations, and reminders reach the right people over in-app, email, and Slack, with per-user channel opt-outs by topic.

A tells one person that something needs their attention: a job was assigned to them, an approval is waiting, a gauge's calibration lapsed, a quote expired. Carbon raises these from across the system and fans each one out to up to three channels: the in-app inbox (the bell in the top bar), email, and Slack. In-app is always on; email and Slack are extras a user can mute per topic.

Say a purchasing manager assigns a purchase order to a buyer. The buyer gets a row in their in-app inbox immediately, an email if the company has email notifications, and a Slack DM if the company connected Slack and the buyer hasn't muted purchasing on Slack. If that buyer sent the assignment to themselves, they get nothing — Carbon never notifies you about your own action.

What raises a notification

Every notification starts as a carbon/notify event with a specific type. The type falls into one of three shapes:

FieldType
Someone is put on a record: a job, job operation, sales order, purchase order, RFQ, purchase invoice, supplier quote, issue, risk, procedure, picking list, stock transfer, maintenance dispatch, or training. The assignee is the recipient.
A record's state moved in a way someone should know about: an approval was requested, approved, or rejected; a job completed; a change order entered a new stage; a quote expired; a customer or supplier responded to a digital quote or supplier quote.
Time-driven, raised by scheduled jobs rather than a user action: a gauge calibration expired, a maintenance dispatch was auto-created for a work center, or a weekly training reminder rolls up a person's outstanding trainings.

The full set of event types lives in NotificationEvent (packages/notifications/src/index.ts:7). Adding a notification anywhere in Carbon means dispatching one of these — there's no free-form "send a message" path.

NOTE

Notifications are informational, not a workflow. Marking one read doesn't change any record, and there's no "acknowledge" or "accept" on a notification itself. The action lives on the underlying document (approve the PO, complete the job) — the notification just points you there.

Topics

Every event maps to exactly one topic — a coarse bucket used for grouping the inbox and for the per-user channel opt-outs. There are twelve, and the mapping is fixed in code (getNotificationTopic, packages/notifications/src/index.ts:114):

TopicExample events
ApprovalApproval requested / approved / rejected
JobJob assignment, job operation assignment, job operation message, job completed
SalesSales order assignment, sales RFQ assignment, RFQ ready
QuoteQuote assignment, quote expired, digital quote response, supplier quote assignment / response
PurchasingPurchase order, purchasing RFQ, and purchase invoice assignments
InventoryPicking list and stock transfer assignments
QualityIssue assignment, risk assignment
MaintenanceMaintenance dispatch (assigned or created), gauge calibration expired
TrainingTraining and procedure assignments, resource training, training reminder
SuggestionSuggestion response
ItemsChange order started / in implementation / complete
GeneralAnything without a specific bucket
HEADS UP

The topic string is stored on every notification (notification.topic). It's what a user's opt-out row matches on, so a topic is not just a label — it's the grouping key for muting. Eleven of the twelve topics are user-facing on the settings page; only Items is not offered as a toggle.

Channels

A single notification can go to three destinations, defined in NotificationDestination (packages/notifications/src/index.ts:108):

FieldType
The bell in the top bar. Always written for every recipient, on every event. It cannot be turned off — the fan-out adds it regardless of what the caller requests (notify.ts:210).
A per-notification email with a heading, a short description, and a View button linking to the record. Sent only when the company's plan includes email notifications.
A direct message in the connected Slack workspace. Sent only when the company has an active Slack integration and the recipient has a matching Slack account.

Each event has default channels (most assignments default to email + Slack), and the caller can override them per dispatch. In-app is the constant: it's the one channel a user can never silence and the one the top bar always reflects.

HEADS UP

Email is plan-gated. The fan-out checks the EMAIL_NOTIFICATIONS plan feature and skips the email channel entirely for companies without it (notify.ts:509). Slack has no plan gate — it only needs the integration connected. See licensing for what each plan includes.

Per-user preferences

Each user controls their own email and Slack delivery under Account → Notifications. The settings page is a grid: one row per user-facing topic, a column per channel, a switch in each cell. In-app has no switch — it's not something a user opts out of.

Preferences are stored as opt-out rows, one per (user, company, channel, topic) (notificationPreference table). The default is on: no row means the channel is enabled for that topic. Flipping a switch off writes a row with enabled = false, which mutes that one topic on that one channel. Flipping it back on removes the mute. The fan-out reads these rows and drops muted recipients from the email and Slack lists before sending (notify.ts:302); the in-app row is never filtered.

NOTE

Muting is per topic and per channel, scoped to the current company. Muting Purchasing on Slack still leaves you Purchasing emails, still leaves you Slack DMs for Sales, and doesn't carry over to another company you belong to. And whatever you mute, the in-app inbox still gets every notification.

The Slack column only appears when the company has Slack connected. The email column always shows; if the company's plan doesn't include email notifications, the page notes that email won't be delivered even with the switch on.

Who receives an expiration or reminder

Assignments and responses have an obvious recipient — the assignee, or the person who raised the request. The time-driven events don't, so each resolves its own audience:

FieldType
Goes to a company-configured list of users — the gaugeCalibrationExpiredNotificationGroup array on company settings — not to a role or a single owner. Set it under gauge/calibration settings so the right people hear when a gauge lapses.
Goes to every employee attached to the dispatch's work center. Auto-created dispatches notify whoever staffs that station.
A weekly digest per person, rolling that individual's outstanding trainings into one notification rather than one per course.
NOTE

There's no general "notification group" entity. Only gauge-calibration expiry uses a configured user list; everything else derives recipients from the record itself — the assignee, the approvers, the work center's staff, or the person with outstanding trainings.

The inbox

The bell in the top bar opens the in-app inbox. It shows recent notifications newest-first with an unread count, each row carrying an icon for its event, a short description, who it came from, and a timestamp. Click a row's link to jump to the record; mark a row read with its mark read button.

NOTE

Recurring reminders don't nag forever. A repeating email notification for the same document stops after a few successful deliveries (MAX_NOTIFICATION_DELIVERIES, packages/notifications/src/index.ts:90) — currently only the weekly training reminder is recurring. The in-app row still stands until you read it.