Platform Reference

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.

ChannelKeyConfig RequiredSupports TemplatesRate Limit
In-Appin_appNone — always available100 / user / min
EmailemailSMTP or SES/Sendgrid credentials60 / recipient / hr
SlackslackOAuth app install or Bot Token50 / channel / min
MS TeamsteamsIncoming Webhook URL per channel50 / channel / min
WebhookwebhookTarget URL (per notification)10 / endpoint / sec
SMSsmsTwilio Account SID + Auth Token10 / number / min
PagerDutypagerdutyIntegration keyUnlimited

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.

incident-assigned (email)
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 ITSM

Built-in Variables

The following variables are always injected and do not need to be supplied in the variables map:

VariableValue
{{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.

incident-assigned (slack variant)
{
  "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

FieldTypeRequiredDefaultDescription
namestringrequiredHuman name for the rule.
eventstringrequiredEvent type to match, e.g. incident.assigned or sla.breach.
recipientsobjectrequiredWho to notify: { role?, team?, user?, reporter?, assignee? }.
channelsstring[]requiredOrdered list of channel keys. First available channel is used.
templatestringrequiredTemplate slug to render.
conditionsobjectoptionalFilter object limiting when the rule fires (e.g. { severity: "P1" }).
quiet_hoursobjectoptional{ start: "22:00", end: "08:00", timezone: "America/New_York" } — defer to start of next active window.
throttleobjectoptional{ max: 3, window: "1h" } — suppress after N sends per recipient per window.
enabledbooleanoptionaltrueToggle rule without deleting.

Example Rule

json
{
  "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.
P1 incident pages and SLA breach alerts bypass user quiet hours. Critical operational alerts are never silenced.

Sending API

POST
/api/v1/notifications/send

Send a notification to one or more recipients.

json
// 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..."
  }
}
POST
/api/v1/notifications/broadcast

Send to all members of a role or team.

GET
/api/v1/notifications

List notifications for the current user.

POST
/api/v1/notifications/:id/read

Mark a notification as read.

GET
/api/v1/notifications/delivery-log

Delivery 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

typescript
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,
  },
})
OptionRequiredDescription
tenantIdrequiredTenant ID.
workspaceIdrequiredWorkspace ID. Scopes the notification to the workspace inbox and the workspace-scoped notification center.
userIdrequiredRecipient user ID or ObjectId.
typerequiredNotification type slug. Used for filtering and routing rules (e.g. incident.assigned, sla.breach).
titlerequiredShort notification title shown in the inbox header.
bodyrequiredNotification body text.
linkoptionalRelative URL to navigate to when the notification is clicked.
metaoptionalArbitrary metadata object stored with the notification for querying.

Reading notifications

typescript
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)
From SDK scripts and custom nodes, use ctx.notify.send() instead of importingnotification.service.ts directly — the SDK method handles tenant/workspace context automatically from the run context.