Notification System
Channels, templates, delivery rules, per-user preferences, and the programmatic sending API — everything you need to route the right alert to the right person.
FlowOS notifications flow through a central delivery engine that handles channel routing, template rendering, rate limiting, and delivery tracking. You can trigger notifications from workflows via the Notify node, from the Flow SDK via ctx.notify.send(), or directly from the REST API.
Channels
Each channel must be configured in Settings → Notifications → Channels before it can deliver messages.
| Channel | Key | Config Required | Supports Templates | Rate Limit |
|---|---|---|---|---|
| In-App | in_app | None — always available | ✓ | 100 / user / min |
| SMTP or SES/Sendgrid credentials | ✓ | 60 / recipient / hr | ||
| Slack | slack | OAuth app install or Bot Token | ✓ | 50 / channel / min |
| MS Teams | teams | Incoming Webhook URL per channel | ✓ | 50 / channel / min |
| Webhook | webhook | Target URL (per notification) | — | 10 / endpoint / sec |
| SMS | sms | Twilio Account SID + Auth Token | — | 10 / number / min |
| PagerDuty | pagerduty | Integration key | — | Unlimited |
Templates
Notification templates live in Settings → Notifications → Templates. Each template is a named, versioned object with per-channel variants. Channels without an explicit variant fall back to the default variant.
Variable System
Templates use double-brace Handlebars syntax. Variables are resolved at send time from the variables map you pass.
Subject: [{{severity}}] {{incidentNumber}} assigned to you — {{incidentTitle}}
Hi {{recipientName}},
Incident {{incidentNumber}} has been assigned to your team.
Title: {{incidentTitle}}
Severity: {{severity}}
Status: {{status}}
Link: {{incidentUrl}}
{{#if notes}}
Notes from the assigner: {{notes}}
{{/if}}
-- FlowOS ITSMBuilt-in Variables
The following variables are always injected and do not need to be supplied in the variables map:
| Variable | Value |
|---|---|
| {{recipientName}} | Recipient's display name |
| {{recipientEmail}} | Recipient's email address |
| {{workspaceName}} | Your workspace name |
| {{workspaceUrl}} | Base URL of your FlowOS instance |
| {{sentAt}} | ISO 8601 send timestamp in recipient timezone |
| {{notificationId}} | Unique notification ID for tracking |
| {{unsubscribeUrl}} | One-click unsubscribe URL (email only) |
Channel Variants
A template can have separate bodies for each channel. For Slack you write Block Kit JSON; for Teams you write Adaptive Card JSON; for email you write HTML or Markdown; for in-app you write a short Markdown string.
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*[{{severity}}] {{incidentNumber}}* — {{incidentTitle}}"
}
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Status:* {{status}}" },
{ "type": "mrkdwn", "text": "*Assigned to:* {{recipientName}}" }
]
},
{
"type": "actions",
"elements": [
{ "type": "button", "text": { "type": "plain_text", "text": "View Incident" }, "url": "{{incidentUrl}}" }
]
}
]
}Delivery Rules
Delivery rules determine which channel to use for which event type and recipient. They are evaluated in priority order and the first matching rule wins.
Rule Structure
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | required | — | Human name for the rule. |
| event | string | required | — | Event type to match, e.g. incident.assigned or sla.breach. |
| recipients | object | required | — | Who to notify: { role?, team?, user?, reporter?, assignee? }. |
| channels | string[] | required | — | Ordered list of channel keys. First available channel is used. |
| template | string | required | — | Template slug to render. |
| conditions | object | optional | — | Filter object limiting when the rule fires (e.g. { severity: "P1" }). |
| quiet_hours | object | optional | — | { start: "22:00", end: "08:00", timezone: "America/New_York" } — defer to start of next active window. |
| throttle | object | optional | — | { max: 3, window: "1h" } — suppress after N sends per recipient per window. |
| enabled | boolean | optional | true | Toggle rule without deleting. |
Example Rule
{
"name": "P1 incidents — page on-call via PagerDuty",
"event": "incident.created",
"conditions": { "severity": "P1" },
"recipients": { "role": "on-call-engineer" },
"channels": ["pagerduty", "slack", "in_app"],
"template": "incident-created-p1",
"throttle": { "max": 1, "window": "5m" }
}User Preferences
Each user can manage their own notification preferences at Settings → Notifications. Preferences override delivery rules for that individual.
- •Subscribe / Unsubscribe: Opt in or out of each event type.
- •Channel Override: Always use email even if the rule says Slack.
- •Quiet Hours: Personal do-not-disturb schedule per channel.
- •Digest Mode: Batch non-urgent notifications into a daily summary email.
- •Out of Office: Redirect notifications to a backup user while away.
Sending API
/api/v1/notifications/sendSend a notification to one or more recipients.
// Request body
{
"channel": "email",
"to": "usr_01HX...", // user ID, email address, or team slug
"template": "incident-assigned",
"variables": {
"incidentNumber": "INC-1042",
"incidentTitle": "API gateway 503 errors",
"severity": "P1",
"status": "investigating",
"incidentUrl": "https://app.flowos.io/itsm/inc_01HX..."
}
}/api/v1/notifications/broadcastSend to all members of a role or team.
/api/v1/notificationsList notifications for the current user.
/api/v1/notifications/:id/readMark a notification as read.
/api/v1/notifications/delivery-logDelivery log for audit (admin only).
Delivery Log
Every notification attempt is logged with status, channel, timestamp, and any error message. Access the delivery log at Settings → Notifications → Delivery Log or via the API.
- •
queued— queued for delivery, not yet sent. - •
sent— accepted by the channel provider. - •
delivered— delivery confirmed (where supported, e.g. email read receipts). - •
bounced— permanent delivery failure (invalid address). - •
failed— transient failure, may be retried. - •
suppressed— blocked by user preference, quiet hours, or throttle rule.
Programmatic API
Backend services, SDK scripts, jobs, and plugins send notifications through thenotification.service.ts helper. All functions are workspace-scoped — notifications are stored in root_<tenantId>_<workspaceId>_sys_notifications and are only visible to users within that workspace.
sendNotification
import { sendNotification } from '../../services/notification.service.js'
await sendNotification({
tenantId: 't_acme',
workspaceId: 'ws_prod', // required — scopes to the workspace inbox
userId: incident.assignedTo,
type: 'incident.assigned',
title: `Incident ${incident.number} assigned to you`,
body: `${incident.title} — priority: ${incident.priority}`,
link: `/itsm/${incident._id}`,
meta: {
incidentId: incident._id,
incidentNumber: incident.number,
severity: incident.severity,
},
})| Option | Required | Description |
|---|---|---|
| tenantId | required | Tenant ID. |
| workspaceId | required | Workspace ID. Scopes the notification to the workspace inbox and the workspace-scoped notification center. |
| userId | required | Recipient user ID or ObjectId. |
| type | required | Notification type slug. Used for filtering and routing rules (e.g. incident.assigned, sla.breach). |
| title | required | Short notification title shown in the inbox header. |
| body | required | Notification body text. |
| link | optional | Relative URL to navigate to when the notification is clicked. |
| meta | optional | Arbitrary metadata object stored with the notification for querying. |
Reading notifications
import {
getNotifications,
markAsRead,
markAllAsRead,
} from '../../services/notification.service.js'
// Fetch all unread notifications for a user in a workspace
const unread = await getNotifications(tenantId, workspaceId, userId)
// returns NotificationResult[] ordered newest-first
// Mark one as read
await markAsRead(tenantId, workspaceId, notificationId)
// Mark all as read for a user
await markAllAsRead(tenantId, workspaceId, userId)ctx.notify.send() instead of importingnotification.service.ts directly — the SDK method handles tenant/workspace context automatically from the run context.