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.
| Mode | When a user enters |
|---|---|
| Audience | When 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. |
| Inactivity | After a configurable period of no app-open (afterMs). Per-user quiet timer — useful for win-back flows. |
| Manual | Nobody 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. |
#Re-entry and guards
Every entry node carries three guardrails you can tune:
| Setting | Options | Effect |
|---|---|---|
| Re-entry | Once · Multiple · Cooldown (duration) | Controls how many times the same user can enter. Cooldown lets a user re-enter after a configurable quiet period. |
| Entry limit | Optional cap (integer) | Stops new entries once the flow has accepted N users total. |
| Exit criteria | Any audience condition | An always-on guard evaluated continuously. A user matching this condition is pulled out of the flow immediately, regardless of which step they are on. |
#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.
| Node | Output ports | What it does |
|---|---|---|
| Entry | out | The flow's entry point. Configures the trigger, re-entry policy, exit criteria, and optional frequency cap. |
| Message | out | Sends 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. |
| Delay | out | Waits a fixed duration (mode: "duration") or holds the user until the next allowed delivery window (mode: "until_window"). |
| Wait-until | met · timeout | Pauses 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. |
| Branch | yes · no | Evaluates 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. |
| Update | out | Sets one or more user attributes. Assignments take a property name and a literal value (string, number, or boolean). |
| Webhook | out · error | Makes 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.
{
"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:
- Type a description and press Generate (or Enter). The request goes to
POST /api/flows/generate. - 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.
- If validation passes (green check), the Activate button is enabled. Validation errors block activation; warnings are shown but do not block.
- Clicking Activate calls
POST /api/flows/activatewhich persists the flow and makes it live.
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:
| Status | What it means |
|---|---|
| Live | The flow is active. Users enter on the next matching event or cron tick, and in-flight users keep advancing through their steps. |
| Paused | Stops 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. |
| Draft | The 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. |
Next: Flow templates — start from a pre-wired growth sequence instead of a blank canvas.