Skip to main content
A flow is the graph that defines an experience: a set of { nodes, edges } you build in the visual editor. At runtime the backend walks that graph server-side starting from the trigger, following edges, resolving branches, running action nodes and, for guides, composes the finished widget at the end and pushes it to the browser. You never run the graph in the browser. The browser SDK is a passive consumer: it renders what the server sends. Understanding the flow model is mostly about understanding what the server does with the graph you draw.

How it works

Every experience stores two graph fields. You only ever edit one of them.
  • flow the editor graph, exactly as React Flow serializes it: { nodes, edges, viewport }. It carries UI chrome: the visual start/end markers, empty-branch ”+” placeholders, and the add-node buttons. This is what the flow editor saves on every change.
  • flowRuntime the normalized graph the runtime actually walks. The backend derives it from flow on every save via flowEditorToRuntime(): it drops the visual-only chrome nodes, drops the __add__ / __add_edge__ helpers, validates that edges point at real nodes, and bridges edges through removed chrome so a branch that visually routed through an end-marker still connects to the node that follows it.
That split matters because the editor needs decorative nodes to feel good, but the runtime needs a clean directed graph. You author flow; the platform keeps flowRuntime in sync. There is no situation where you write flowRuntime by hand. The runtime walk happens inside the conversation cycle. When an observed message arrives, the backend resolves which experiences are eligible, then for a guide walks that experience’s flowRuntime from the trigger to a terminal node, executing each node it passes: Multi-step experiences (tours and surveys) work the same way, except the user drives navigation. When the rendered widget emits flow.next or flow.back, that intent travels back over Socket.IO and the server advances the step, fires any deferred action nodes for the new step, composes the next widget, and pushes it. The browser never decides where the flow goes. See Realtime for the transport detail.

Node types

Which nodes you can add depends on the experience type (guide | tour | survey | announcement), chosen at creation and immutable thereafter. The authoring catalog every node, its defaults, and where it’s available is defined in one registry, so the palette you see is always the set of nodes that are valid for that experience. Flow-level nodes are available across experience types and form the routing and side-effect backbone: Guides terminate at a Result / UI Widget node the terminal node holding the instruction (or an authored widget) that the backend turns into a rendered widget. Tours, surveys, and announcements add content nodes on top of this backbone: message-style nodes (message, card, rich card, quick replies, carousel) for tours and announcements, and survey-question nodes (scale, multiple choice, open question, checklist, interview prompt, text block, thank you) for surveys. Each content node produces the instruction or content for one step of the experience.

Trigger

The trigger is the single entry point, created automatically when you open a new flow you cannot add or delete it. It does not decide whether the experience runs; that is the experience-level trigger and audience configuration evaluated before the walk begins. Inside the graph it is simply the node every walk starts from.

Action nodes

api_call, webhook, and slack_message are action nodes: they perform a server-side side effect when the walk reaches them. An api_call runs an HTTP GET and exposes the response to nodes downstream; webhook and slack_message deliver outbound notifications. Each ships with sensible defaults for example, an api_call defaults to a 5-second timeout and a 50 KB response cap:
In multi-step experiences, webhook and slack_message actions can run inline during the walk or be deferred until the user actually reaches that step via flow.next. Deferral exists so a notification fires when a user genuinely arrives at a step, not when the flow is first composed. The runtime buckets deferred actions by the step they precede and fires a bucket the first time its step is shown.

Branching: classify vs. decision

Both classify and decision nodes split a flow into branches. They share the same branching mechanics on the canvas branches use AND within a branch, OR across branches, and the first matching branch wins but they resolve those branches completely differently. A decision node is deterministic. Its branches carry conditions evaluated against traits.* and answers.* using the same operator set as triggers (equals, contains, in, gt, exists, and so on see the operators reference). It is legacy and hidden from the palette; new flows should branch with classify. A classify node is resolved by an LLM at runtime through the backend’s branch-decision layer. You configure the provider and model, the instructions that tell the model how to choose, a confidence floor below which the result is rejected, a fallback branch taken on low confidence or error, a resolution mode, a TTL, and a signal mask that controls what the model sees: Because classification runs server-side, the model never sees your secrets in the browser and the branch decision cannot be tampered with from the client. A classify node’s resolution is also recorded per walk the raw pick, the effective (sticky) branch, whether the intent switched, and whether the fallback was used so you can see why a flow went the way it did in Analytics.

Terminating a guide

A guide’s walk ends at its terminal node, which yields one of two things. A plain Result node yields an instruction: free text that WidgetCompositionService sends to Anthropic (claude-sonnet-4-6), which renders a WidgetTree validated against the widget schema. A UI Widget node instead carries an authored WidgetTree directly you composed the blocks yourself, and the backend uses them as-is (filling any declared slots) rather than asking the model to invent layout.
Use a Result node when you want the model to compose freely from the conversation; use a UI Widget node when you need a fixed, predictable layout. Either way the output is a WidgetTree of blocks pushed to the browser over Socket.IO the browser SDK only ever renders it.

Specific cases and limitations

flowRuntime is derived on every save by stripping editor chrome from flow and validating edges. If flowRuntime ever lags flow (an older row, a partial write), the public config endpoint prefers the editor flow so new node fields are not shadowed by a stale snapshot. Treat flowRuntime as a read-only build artifact.
The visual ”+” placeholder on an unfinished branch is chrome and is stripped at runtime. A branch you left empty simply routes nowhere the walk stops there. This is by design: it keeps the rest of the flow runnable instead of failing the whole graph because one branch was incomplete.
The trigger node is the graph’s entry point. Whether the experience runs at all event type, conditions, audience, schedule, frequency is the experience-level configuration evaluated before the walk. See Triggers & audience.

Next

Once a flow reaches its terminal node, the output is a tree of block primitives — see Widgets & blocks. To control when a flow runs in the first place, configure Triggers & audience.