{ 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.flowthe 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.flowRuntimethe normalized graph the runtime actually walks. The backend derives it fromflowon every save viaflowEditorToRuntime(): 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.
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 experiencetype
(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:
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
Bothclassify 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 thatWidgetCompositionService 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.
WidgetTree of blocks pushed to the
browser over Socket.IO the browser SDK only ever renders it.
Specific cases and limitations
You edit `flow`, never `flowRuntime`
You edit `flow`, never `flowRuntime`
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.An empty branch routes to nothing
An empty branch routes to nothing
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 not your trigger configuration
The trigger node is not your trigger configuration
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.