> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Flows & nodes

> How a flow graph defines an experience, how the backend walks it at runtime, and what each node type does.

A **flow** is the graph that defines an [experience](/concepts/experiences): 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](/react/realtime): 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](/features/authoring) 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](/concepts/conversations-and-sessions).
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:

```mermaid theme={null}
flowchart TD
  T["Trigger<br/>entry point"] --> C{"Classify<br/>LLM branch"}
  C -->|"Class A"| A["API Call<br/>fetch order status"]
  C -->|"Otherwise"| W2["UI Widget<br/>generic help"]
  A --> R["Result / UI Widget<br/>terminal node"]
  R --> COMP["WidgetCompositionService<br/>Anthropic claude-sonnet-4-6"]
  COMP --> PUSH["Socket.IO<br/>widget.show → browser"]

  classDef entry fill:#8b5cf6,stroke:#6d28d9,color:#fff;
  classDef branch fill:#0d9488,stroke:#0f766e,color:#fff;
  classDef action fill:#6366f1,stroke:#4338ca,color:#fff;
  classDef result fill:#059669,stroke:#047857,color:#fff;
  classDef infra fill:#f97316,stroke:#c2410c,color:#fff;
  class T entry;
  class C branch;
  class A,W2 action;
  class R result;
  class COMP,PUSH infra;
```

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](/react/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:

| Node              | Palette type    | What it does                                                                                                                                                |
| ----------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Trigger**       | `trigger`       | The entry point. Auto-created at flow start, not manually addable. Fires when the experience's [trigger conditions](/concepts/triggers-and-audience) match. |
| **Classify**      | `classify`      | LLM-based branching. Routes the flow by reading the conversation against your instructions, with a confidence floor and a fallback branch.                  |
| **Decision**      | `decision`      | Rule-based branching. Legacy and hidden from the palette by default; still resolves so older flows keep working.                                            |
| **API Call**      | `api_call`      | Server-side HTTP GET. The response feeds downstream nodes. See [API calls](/integrations/api-calls).                                                        |
| **Webhook**       | `webhook`       | Sends an HTTP request to your endpoint. See [Webhooks](/integrations/webhooks).                                                                             |
| **Slack Message** | `slack_message` | Posts a message or DM to Slack. See [Slack](/integrations/slack).                                                                                           |
| **UI Widget**     | `widget`        | Composes a custom widget from [block primitives](/concepts/widgets-and-blocks). Carries an authored `WidgetTree`.                                           |

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](/concepts/triggers-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:

```ts theme={null}
// api_call node defaultConfig (from the flow node registry)
{
  apiCallUrl: "",
  apiCallAuthHeader: "Authorization",
  apiCallTimeoutMs: 5000,
  apiCallMaxResponseBytes: 51200,
}
```

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](/concepts/triggers-and-audience)). 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:

| Field                | Meaning                                                                                   |
| -------------------- | ----------------------------------------------------------------------------------------- |
| `provider` / `model` | Which LLM resolves the branch (`openai` or `anthropic`).                                  |
| `instructions`       | Natural-language rubric for picking a branch.                                             |
| `confidenceFloor`    | Minimum confidence (0–1) to accept the model's pick; below it, the fallback is used.      |
| `fallbackBranchId`   | Branch taken on low confidence or a classification error.                                 |
| `resolutionMode`     | `blocking` waits for the decision before composing; `optimistic` proceeds and reconciles. |
| `ttlMs`              | How long a resolved decision stays sticky before re-resolving.                            |
| `signalMask`         | What the model sees e.g. `includeMessage`, `includeConversationDigest`.                   |

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](/features/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.

```ts theme={null}
// UI Widget node defaultConfig an authored WidgetTree (schema v2)
{
  widget: {
    schemaVersion: 2,
    widgetId: "widget",
    header: { title: "Widget" },
    block: {
      type: "stack",
      children: [{ type: "text", text: "Text" }],
    },
  },
}
```

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](/concepts/widgets-and-blocks) pushed to the
browser over Socket.IO the browser SDK only ever renders it.

## Specific cases and limitations

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](/concepts/triggers-and-audience).
  </Accordion>

  <Accordion title="Navigation is server-side">
    `flow.next` and `flow.back` are intents the rendered widget emits; the server
    advances the step and pushes the next widget. The browser cannot skip ahead or
    branch on its own it has no copy of the graph to walk.
  </Accordion>
</AccordionGroup>

## Next

Once a flow reaches its terminal node, the output is a tree of block primitives —
see [Widgets & blocks](/concepts/widgets-and-blocks). To control *when* a flow runs
in the first place, configure [Triggers & audience](/concepts/triggers-and-audience).
