Skip to content

Alert Flow Spec ​

An AlertFlow is the data structure behind every alert built in the Alerts Creator. It is a tree of steps, not a node/edge graph: each flow has a single trigger and an ordered list of steps evaluated top to bottom.

Canonical types live in app/src/shared/types/alert-flow.ts; evaluation lives in app/src/main/alerts/flowEvaluator.ts.

AlertFlow ​

typescript
interface AlertFlow {
  id: string;
  name: string;
  enabled: boolean;
  trigger: AlertTrigger;
  root: Step[];
  createdAt: string;
  updatedAt: string;
}

Only flows with enabled: true are loaded and bound at runtime. The root array is the entry point of evaluation.

AlertTrigger ​

typescript
interface AlertTrigger {
  type: AlertTriggerType;
  params?: Record<string, unknown>;
}
typeSource pathparams
followEventSub-
subscribeEventSub-
cheerEventSub-
raidEventSub-
gift_subEventSub-
welcomeEventSub-
channel_pointFlowEngine (redemption){ rewardId } (omit = any reward)
cronFlowEngine (scheduler){ schedule, onlyWhileLive }

For cron, onlyWhileLive defaults to true (the job only fires while the stream is online). For channel_point, omitting rewardId makes the flow fire on every custom reward redemption.

Step ​

A Step is one of three discriminated kinds:

typescript
type Step =
  | { kind: "display"; id: string; display: AlertDisplay }
  | {
      kind: "switch";
      id: string;
      variable: string;
      cases: { match: string; steps: Step[] }[];
      default: Step[];
    }
  | {
      kind: "if";
      id: string;
      variable: string;
      op: ConditionOp;
      value: string;
      then: Step[];
      else: Step[];
    };
  • display - pushes its AlertDisplay onto the output list.
  • switch - resolves variable against the event context, stringifies it, then runs the first case whose match equals that string (exact string match). If none match, runs default.
  • if - compares variable against value using op, then runs then or else.

Steps nest freely: a switch case or if branch contains its own Step[].

ConditionOp ​

typescript
type ConditionOp = "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains";

Comparison semantics (compare() in flowEvaluator.ts): operands are stringified first. eq / neq / contains always operate on strings. For gt / lt / gte / lte, if both operands parse as finite numbers the comparison is numeric; otherwise it falls back to lexicographic order (so "9" > "10" is true). A missing variable resolves to the empty string.

AlertDisplay ​

typescript
interface AlertDisplay {
  phrase: string;
  htmlTemplate?: string;
  assetId?: string;
  imageUrl?: string; // resolved at runtime, never persisted
  durationMs: number;
  volume: number; // 0..1
  animaleseEnabled?: boolean;
  animalesePitch?: number;
}

assetId references an asset from the Assets Manager. At fire time, resolveDisplayImage() looks the asset up and sets imageUrl to the file basename the overlay can load. See the Asset Protocol. imageUrl is never stored in SQLite.

Evaluation ​

evaluateFlow(flow, ctx) walks flow.root and returns an ordered AlertDisplay[]. It is pure and never throws: an unresolved variable, an unmatched switch with an empty default, or a malformed branch simply yields fewer displays. Steps execute in array order; the runtime currently emits only the first display produced.

The evaluation context is keyed under event, for example event.tier, event.userName, event.bits. TRIGGER_VARIABLE_HINTS lists the variables surfaced per trigger in the editor picker (all variables stay accessible regardless).

Execution paths ​

Two paths feed evaluateFlow, both in app/src/main/handlers/twitch.ts:

  1. EventSub path (processAlert) - follow / subscribe / cheer / raid / gift_sub / welcome. An incoming Twitch event is matched against enabled flows of the same trigger.type; a matching flow decorates the alert with its display. Flow-only types (channel_point, cron) are rejected here.
  2. Flow trigger path (handleFlowTrigger, action alerts:flow_trigger) - channel_point and cron flows are bound by the FlowEngine (app/src/main/flows/engine.ts), which dispatches twitch / alerts:flow_trigger on fire. The editor test button uses the same path with a synthetic TestUser event. Here the flow is the source: if it produces no display, nothing is emitted.

The resulting alert carries TwitchAlertData.flowId (the id of the flow that produced the display). A flow-scoped overlay can filter on it.

Overlay flow filter ​

The built-in twitch-alerts overlay reads a ?flow=<id> query parameter. With it set, the overlay only enqueues alerts whose lastAlert.flowId equals that id; without it, the overlay shows every alert (shared queue). This lets a single flow drive a dedicated OBS browser source.

Example flow ​

A subscription flow that switches on the tier:

json
{
  "id": "flow_sub_tiers",
  "name": "Sub tiers",
  "enabled": true,
  "trigger": { "type": "subscribe" },
  "root": [
    {
      "kind": "switch",
      "id": "s1",
      "variable": "event.tier",
      "cases": [
        {
          "match": "3000",
          "steps": [
            {
              "kind": "display",
              "id": "d3",
              "display": {
                "phrase": "{{userName}} just went Tier 3!",
                "assetId": "tier3gifAB",
                "durationMs": 6000,
                "volume": 0.9
              }
            }
          ]
        },
        {
          "match": "2000",
          "steps": [
            {
              "kind": "display",
              "id": "d2",
              "display": {
                "phrase": "{{userName}} subbed at Tier 2!",
                "durationMs": 5000,
                "volume": 0.8
              }
            }
          ]
        }
      ],
      "default": [
        {
          "kind": "display",
          "id": "d1",
          "display": {
            "phrase": "Thanks for the sub, {{userName}}!",
            "durationMs": 4000,
            "volume": 0.7
          }
        }
      ]
    }
  ],
  "createdAt": "2026-06-07T10:00:00.000Z",
  "updatedAt": "2026-06-07T10:00:00.000Z"
}

EventSub sets event.tier to "1000", "2000" or "3000". Tier 1 falls through to default.

Released under the MIT License.