Skip to main content
A flow reads like a sentence: when a user adds their first connection and then adds no second one for 48 hours, send “Your network is waiting”. You can build flows on the Flows page, approve them from a Composer proposal, or have your own AI agent write them over MCP. They all end up in the same format, checked by the same rules.

Lifecycle

Nothing sends until a person activates a flow. Only owners and admins can.

Steps

A flow’s steps are a pattern over one user’s events. Each user moves through the steps on their own. When they reach the end, the flow sends.
The user does something. Optional fields:
  • where: conditions the event must meet
  • withinSeconds: a deadline. If the event hasn’t happened by then, the user leaves the flow.
  • afterSeconds: the event only counts once this much time has passed
  • bind: save values from the event for later steps or for the message
The first step is always an await: a flow starts when something happens.
The user does not do something for forSeconds. This is how you write “hasn’t opened the app in 3 days”. The clock starts when the user reaches this step.
Pause for forSeconds. This is how you write “2 hours after”.

Example

“A user added their first connection, then added none for 48 hours”:
maxRunAgeSeconds is the longest a user can sit part-way through the flow before they’re dropped. Set it well above the total of the steps’ windows.

Branches

Steps can also branch. Give a step a next list of the step ids that may follow it. The first one satisfied wins. Each branch ends on a send step with its own message. Without next on any step, steps simply run in order.

Conditions

A where list holds conditions on the event’s properties. All of them must be true.
property can be a dotted path into nested properties, like metadata.tier. To compare against a value saved by an earlier step, use valueRef instead of value, for example “the same order_id as the cart event”.
Types are never converted. "80" (a string) is not 80 (a number), so { "op": "gte", "value": 80 } never matches a property sent as "92". A missing property never matches either, except with not_exists.

Messages

Title and subtitle are at most 120 characters, and the body at most 400. A variable is written as {{ path }}, and can read: A send whose variable is missing is skipped, not sent with a blank. “Nice work, !” is worse than no push. The skip is logged with its reason.

Custom data

data holds keys and values for your app’s own code, like which screen to refresh. It is delivered with the push but never shown to the user. Leave it out, or set it to null, when the message has none. Read it in your app.
  • Values are text and can use variables, the same as the copy. A value whose variable is missing skips the send, like any other field.
  • At most 20 keys per message. A value is at most 256 characters.
  • A key is letters, digits and underscores, starts with a letter or underscore, and is at most 64 characters.
  • aps, pnai and deep_link are used by the push itself, so they can’t be keys.
A push can be at most 4 KB, which Apple enforces. Variables are filled in when the flow decides to send, so a long value can push a message over that limit even though the flow saved fine. Such a send is cancelled before it goes out, with the reason message_too_large.

When it’s filled in

The message is filled in when the flow decides to send. A send delayed by quiet hours still goes out with the copy and data from the moment it was decided, even if you edit the flow in between.

Copy tests

Give a send up to 5 message variants. Each send picks one at random and records which, and the flow page compares how each one does. Keep a variant’s key when you reword it, so its history stays attached.

Limits

Every cancelled send is recorded with its reason, so you can see what a flow would have done. See Debug a user.

When flows compete

If one event completes several flows for the same user, the flow with the highest priority sends and the others are recorded as suppressed. Priority defaults to 0. If a user is already part-way through a flow and its first step matches again, onOverlap decides: ignore (the default) keeps the run they’re in, and restart starts them over.

Right before sending

A send scheduled for later is checked again when its time comes: is the flow still active, is the user still under their caps and outside their quiet hours? A send that no longer qualifies is dropped, not sent, and the reason is recorded. Each flow shows at most one notification on a user’s lock screen: a newer send from the same flow replaces an older one still showing.