docs
Documentation menu

Building flows

Compose triggers, delays, branches, and pushes on the canvas.

#The canvas

The flow builder is a node-and-edge canvas. Each flow is a directed acyclic graph (DAG) of typed nodes wired together through named ports. Every flow has exactly one entry node; from there you wire delays, message sends, branches, and other nodes until every path ends at an exit node (or hands off to another flow with a goto).

Drop a node from the left palette, drag the output port of one node to the input of another to create an edge. The canvas saves the layout alongside the logic so it round-trips when you reopen the flow.

#Entry triggers

The entry node controls who joins the flow and when. Three primary trigger modes are available in the picker; two advanced modes are accessible behind an "Advanced" toggle for flows that need them.

ModeWhen a user enters
AudienceWhen the user transitions into a saved audience (membership false → true). Edge-triggered — each user enters at most once per membership change unless re-entry is set to multiple.
InactivityAfter a configurable period of no app-open (afterMs). Per-user quiet timer — useful for win-back flows.
ManualNobody enters automatically. From the entry editor you either "Test on my device" (enters just your own device) or launch the live flow at every user seen on a chosen app version — marketing consent still applies to each.
Event (advanced)When a specific named event fires for the user. An optional inline filter (audience clause) narrows which event occurrences qualify.
Schedule (advanced)On a UTC cron expression — audience membership is re-evaluated at each tick and current members are entered. Re-entry is forced to multiple when a schedule is set.
Note
The "Audience" trigger also supports an optional cron schedule. Setting one on a segment trigger re-evaluates audience membership at each cron tick and re-enters current members — useful for recurring campaigns.

#Re-entry and guards

Every entry node carries three guardrails you can tune:

SettingOptionsEffect
Re-entryOnce · Multiple · Cooldown (duration)Controls how many times the same user can enter. Cooldown lets a user re-enter after a configurable quiet period.
Entry limitOptional cap (integer)Stops new entries once the flow has accepted N users total.
Exit criteriaAny audience conditionAn always-on guard evaluated continuously. A user matching this condition is pulled out of the flow immediately, regardless of which step they are on.
Heads up
Exit criteria evaluate on every event received while the user is in the flow. Use them for conversion events (e.g. "subscription started") so users who convert are not sent the remaining pushes.

#Node types

Ten node types are available. Every node has an id, an optional display name, and a canvas position carried in the IR for layout.

NodeOutput portsWhat it does
EntryoutThe flow's entry point. Configures the trigger, re-entry policy, exit criteria, and optional frequency cap.
MessageoutSends a push notification. Supports multiple content variants (A/B at the message level), an optional delivery time window in the user's timezone, send-time optimisation, and a bypass flag for transactional sends that should skip frequency caps.
DelayoutWaits a fixed duration (mode: "duration") or holds the user until the next allowed delivery window (mode: "until_window").
Wait-untilmet · timeoutPauses the user until a condition is satisfied or a timeout elapses. The met port fires when the condition is true; the timeout port fires if the condition is never met within timeoutMs.
Branchyes · noEvaluates a condition on the current user. Routes to yes when the condition is true, no otherwise.
A/B Split(one port per variant key)Randomly assigns the user to one of N variants using basis-point weights (sum must equal 10 000). Supports an optional experiment config for statistical significance tracking.
UpdateoutSets one or more user attributes. Assignments take a property name and a literal value (string, number, or boolean).
Webhookout · errorMakes an HTTP request (POST, PUT, or GET) to an external URL. Headers and a body template can be provided. Routes to error on a non-2xx response.
Exit(none — terminal)Ends the user's journey in this flow. An optional reason label documents why.
Connect to flow(none — terminal)Hands the user off to the start of another flow (the target's entry trigger is bypassed; its exit criteria still apply). Useful for chaining sequential journeys.

#Message variants and time windows

A Message node holds an array of variants. Each variant has a stable variantId, a display label, a basis-point weight (weightBp, must sum to 10 000 across all variants), and content: a title, body, optional image URL, and optional deeplink.

A delivery time window (timeWindow) restricts the send to specific days and a minute-of-day range in the user's local timezone. If the user reaches the message node outside the window the send is held until the next qualifying slot.

Flow IR — message node excerpt
{
  "type": "message",
  "channel": "push",
  "variants": [
    {
      "variantId": "…",
      "label": "A",
      "weightBp": 10000,
      "content": {
        "title": "Don't lose your streak",
        "body": "One lesson saves your {{ days_left }}-day streak.",
        "deeplink": "myapp://lesson"
      }
    }
  ],
  "timeWindow": {
    "tzMode": "user",
    "daysOfWeek": [1, 2, 3, 4, 5],
    "startMinute": 480,
    "endMinute": 1200
  }
}

#Frequency caps

A flow-level frequency cap limits how many pushes the engine delivers within a sliding time window for a given user. Caps can be scoped to the flow or enforced globally across all flows.

Individual message nodes can set bypassFrequencyCap: true to opt out of the cap — use this for transactional notifications (e.g. a purchase confirmation) that must always go through.

#Build with AI

The AI prompt bar lets you describe a flow in plain English and generates a validated FlowIR proposal before anything is saved. The flow is never silently activated — you always review it first.

The generation pipeline works as follows:

  1. Type a description and press Generate (or Enter). The request goes to POST /api/flows/generate.
  2. A review panel opens showing the proposed nodes (colour-coded by type), the model's reasoning ("Why this flow"), and the validation result from the shared flow validator.
  3. If validation passes (green check), the Activate button is enabled. Validation errors block activation; warnings are shown but do not block.
  4. Clicking Activate calls POST /api/flows/activate which persists the flow and makes it live.
Note
If the model detects that your description implies an audience that does not yet exist, it creates and saves the audience automatically and wires it to the entry node. A confirmation banner in the review panel tells you the audience name and whether it was wired successfully.

Inside an open flow, a separate AI bar lets you edit the existing flow by describing the change. That request goes to POST /api/flows/generate/edit with the current IR attached, and follows the same review-before-apply pattern.

#Saving and status

Your edits save automatically — there is no Save button. The chip in the top bar shows the live save state (saving, all changes saved, or a validation block that pauses saving until you fix it). Each save runs the same validator the AI uses and writes a new version snapshot to Supabase. A separate status pill sets the flow’s lifecycle:

StatusWhat it means
LiveThe flow is active. Users enter on the next matching event or cron tick, and in-flight users keep advancing through their steps.
PausedStops new entries. Users already mid-flow are parked and resume automatically when you set the flow Live again — nobody is dropped. A send already in flight still delivers.
DraftThe flow is authored but inert — no users enter. A brand-new flow goes Live the first time it saves; use the pill to pause it or move it back to Draft.
Heads up
Editing a flow produces a new version. Users currently in the flow run against the version that was active when they entered; existing in-progress users are not migrated to the new version automatically. Editing a paused or draft flow never re-activates it — only the status pill changes a flow’s lifecycle.

Next: Flow templates — start from a pre-wired growth sequence instead of a blank canvas.