> ## 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.

# Experiences

> The deliverable unit in FirstFlow a typed, flow-driven guide, tour, survey, or announcement that the server evaluates and pushes into chat.

An **experience** is the unit of work in FirstFlow: one tour, survey, announcement, or AI guide that you build once and the server delivers to the right users inside a chat UI. Each experience carries a `type` (fixed at creation), a flow graph that describes what to render, trigger rules that decide *when*, and audience targeting that decides *to whom*.

This page explains what an experience is made of, how the server turns a saved experience into a rendered widget, and the rules and limits you work within. For the visual graph itself see [Flows & nodes](/concepts/flows-and-nodes); for the targeting rules see [Triggers & audience](/concepts/triggers-and-audience).

## The four types

The `type` is chosen when you create an experience and is **immutable** afterward you cannot convert a survey into a guide. It controls which node types the flow editor offers and how the experience renders at runtime.

| Type           | What it is                                                                                         | How it's built                                                                                       |
| -------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `guide`        | A single **AI-composed** widget an in-context answer, walkthrough, or dynamic CTA.                 | The server walks a flow graph to a `result` node, then composes a rich widget from the conversation. |
| `tour`         | A multi-step sequence of styled message cards for a guided feature walkthrough.                    | Authored step-by-step (message, card, rich card, quick replies, carousel nodes).                     |
| `survey`       | Structured data collection NPS, scales, multiple choice, open questions answered natively in chat. | Authored from survey-question nodes (scale, multiple choice, open question, checklist, thank-you).   |
| `announcement` | A one-shot in-chat notification or feature banner.                                                 | Authored from announcement and message nodes.                                                        |

The dividing line is composition time. A **guide** is *AI-composed at runtime*: its flow ends in a `result` node holding an instruction, and Anthropic (`claude-sonnet-4-6`) renders the actual [widget](/concepts/widgets-and-blocks) from that instruction plus conversation context. Tours, surveys, and announcements are *authored ahead of time*: each step is a node you styled in the editor, and the server replays them in order.

## How it works

Everything that decides *whether* and *what* to show runs on the FirstFlow backend. The browser SDK is a passive consumer it renders what arrives over the socket and reports activity back. You cannot trigger an experience purely from the client; you emit activity and the server decides.

The cycle starts when your server SDK observes a chat message and posts it to FirstFlow. The backend resolves the trigger type, finds eligible experiences, optionally runs an LLM classifier gate, then composes and pushes a widget.

```mermaid theme={null}
flowchart TD
    A["Host app chat message"] -->|observed by @firstflow/sdk| B["POST /v1/conversations/:id/messages (202)"]
    B --> C["ConversationRouterService"]
    C --> D{"Eligible experience?<br/>trigger + audience + schedule + frequency"}
    D -->|no| Z["nothing pushed"]
    D -->|yes| E{"Classifier / branch<br/>configured?"}
    E -->|LLM gate| F["branch-decisions resolve"]
    E -->|no gate| G
    F --> G["guide: walk flow graph to a result node"]
    G --> H["WidgetCompositionService<br/>Anthropic claude-sonnet-4-6 → WidgetTree"]
    H --> I["RealtimeService pushes widget.show<br/>over Socket.IO to member:workspace:user"]
    I --> J["@firstflow/react renders the WidgetTree"]
    J -->|user clicks flow.next / flow.back| K["FlowNavigationListener (server-side)"]
    K --> H

    classDef server fill:#0d9488,stroke:#0b6b62,color:#fff;
    classDef client fill:#6366f1,stroke:#4f46e5,color:#fff;
    classDef gate fill:#f97316,stroke:#c2570c,color:#fff;
    class B,C,F,G,H,I,K server;
    class A,J client;
    class D,E gate;
```

The steps in order:

<Steps>
  <Step title="Observe a message">
    Your wrapped LLM client (`@firstflow/sdk`) posts each user or assistant message to `POST /v1/conversations/:conversationId/messages`. The endpoint returns `202` and processes fire-and-forget it never blocks your chat response. The browser provider's `conversationId` must equal the server SDK `sessionId` so both sides agree on the conversation. See [Conversations & sessions](/concepts/conversations-and-sessions).
  </Step>

  <Step title="Resolve the trigger and filter eligibility">
    `ConversationRouterService` derives the trigger type from the message role, then `ExperienceFilterService` finds experiences whose `settings.triggers[]` match and whose audience, schedule, and frequency allow a show. Only `active` experiences are eligible.
  </Step>

  <Step title="Run the classifier gate (optional)">
    If a trigger or a branch node declares a classifier, the `branch-decisions` service runs an LLM gate (OpenAI or Anthropic) to decide whether the experience qualifies or which branch to take.
  </Step>

  <Step title="Walk the flow (guides)">
    For a guide, `GuideFlowWalkerService` traverses the `flowRuntime` graph through decisions and action nodes until it reaches a `result` node, which yields the composition instruction. Authored types replay their nodes directly.
  </Step>

  <Step title="Compose and push the widget">
    `WidgetCompositionService` calls Anthropic (`claude-sonnet-4-6`) to render a `WidgetTree`, validates it, and `RealtimeService.pushToUser()` emits `widget.show` over Socket.IO to the room `member:{workspaceId}:{userId}`.
  </Step>

  <Step title="Render and navigate">
    `@firstflow/react` mounts the `WidgetTree`. When the user clicks a button bound to `flow.next` or `flow.back`, the SDK emits the intent; `FlowNavigationListener` advances the step **server-side** and pushes the next tree. The browser only signals; it never computes the next step.
  </Step>
</Steps>

## What an experience stores

An experience persists four JSON fields plus its identity (`name`, `description`, `type`, `status`). The two graph fields are paired: you edit `flow`, and the backend derives `flowRuntime` from it on every save.

<ParamField path="flow" type="FlowData">
  The full editor graph as React Flow's `{ nodes, edges, viewport }`, including visual chrome (the add buttons, end marker, branch placeholders). This is what the canvas loads and what `updateExperience()` writes back.
</ParamField>

<ParamField path="flowRuntime" type="RuntimeFlow">
  The normalized runtime graph the server actually executes. Derived from `flow` by `flowEditorToRuntime()`, which strips editor-only chrome nodes (`__add__`, the `graphEndWidget` end marker, `bigAddButton` placeholders), keeps the `trigger` start node, and reduces each edge to `{ source, target, sourceHandle, targetHandle }`. You never edit this directly.
</ParamField>

<ParamField path="settings" type="ExperienceSettings">
  Triggers, audience, device, schedule, frequency, priority, placement, and presentation. Zod-validated server-side; see the fields below.
</ParamField>

<ParamField path="widgetTree" type="object | null">
  A legacy static widget spec (schema v0/v1). New experiences are flow-driven; this field exists for backward compatibility and is not the path the AI guide composer uses.
</ParamField>

### Settings fields

`settings` is validated against `ExperienceSettingsSchema`. The runtime reads the plural `triggers[]`; the singular `trigger` is a deprecated mirror kept only for older clients.

<ParamField path="triggers" type="ExperienceTriggerRow[]" default="[{ show_on: 'chat_opens' }]">
  The eligibility rows. Each row has a `when` (a `show_on` event plus an optional `classifier`), a list of `conditions` against `traits.*` / `answers.*`, and `actions` to fire on match. `show_on` is one of `chat_opens`, `after_user_message`, `after_user_and_agent_message`, `after_idle` (with `delay_seconds`, 0–300), `custom_event` (with `custom_event_name`), or `conversation_classifier`.
</ParamField>

<ParamField path="audience" type="{ type: 'all' | 'segment'; segment_id?: string }" default="{ type: 'all' }">
  Who is eligible every user, or a named [segment](/concepts/audience). A deleted segment falls back to `all`.
</ParamField>

<ParamField path="device" type="'all' | 'desktop' | 'mobile'" required>
  Restricts which devices can receive the experience.
</ParamField>

<ParamField path="schedule" type="{ start_type; starts_at?; timezone?; expiration_type; expires_at? }" required>
  When the experience is live. `timezone` is required once `starts_at` or `expires_at` is set.
</ParamField>

<ParamField path="frequency" type="{ type: 'once' | 'limited' | 'always'; limit?; period? }" required>
  How often a single user can see it. `limited` uses `limit` over a `period` (`session`, `day`, `week`, or `lifetime`).
</ParamField>

<ParamField path="priority" type="number" default="0">
  Tie-breaker when multiple experiences are eligible for the same trigger. Higher wins.
</ParamField>

<ParamField path="entry_points" type="('slot' | 'slash')[]" default="['slot']">
  Where the experience can surface: in the widget `slot`, or as a `slash` command the user invokes. See [Commands](/react/commands).
</ParamField>

<ParamField path="presentation" type="{ titleMode; experienceTitle?; experienceIcon? }">
  Experience-wide widget header. `titleMode` is `node` (each node's own title fills the header, default), `body` (an experience-level title fills the header, node title moves into the body), or `hidden`. Edited on the START node.
</ParamField>

## Building the flow

A guide's flow is more than a linear sequence it is a graph you assemble from typed nodes. The flow editor's [node registry](/concepts/flows-and-nodes) is the single source of truth for what nodes exist and where they appear. The `trigger` node is auto-created at the flow start; the rest you drop from the palette.

Some nodes are universal (available in every experience type) and some are scoped. The universal building blocks for branching and side effects:

* **Classify** branches the flow by an LLM classification of the conversation. It carries a `classify` config (provider, model, `classifyText`, `confidenceFloor`) and a `branchDecision` config (`instructions`, `fallbackBranchId`, `signalMask`, `ttlMs`, `resolutionMode` of `blocking` or `optimistic`). Branches use AND within a branch and OR across branches.
* **API Call** a server-side HTTP GET that fetches external data; its result feeds downstream nodes. Config: `apiCallUrl`, `apiCallAuthHeader`, `apiCallTimeoutMs` (default 5000), `apiCallMaxResponseBytes` (default 51200). See [API calls](/integrations/api-calls).
* **Webhook** sends an HTTP request to your endpoint, inline or deferred until `flow.next`. Config includes `webhookMethod`, `webhookSecret` (HMAC signing), `webhookHeaders`, and `webhookContinueOnFailure`. See [Webhooks](/integrations/webhooks).
* **Slack Message** posts a channel message or DM. Config: `slackActionType`, `slackChannelId`, `slackMessageText`, `slackContinueOnFailure`. See [Slack](/integrations/slack).
* **UI Widget** composes a custom widget from primitives, carrying an authored `WidgetTree` (schema v2) directly. See [Widgets & blocks](/concepts/widgets-and-blocks).

A guide flow ends at a `result` node the terminal node holding the instruction that `WidgetCompositionService` turns into the rendered widget.

### The three ways to author

<AccordionGroup>
  <Accordion title="Manually in the visual editor">
    Drop nodes from the palette onto the React Flow canvas, wire edges, and edit each node's config in the property panel. Saving calls `updateExperience()` with `{ flow, settings? }`; the backend re-derives `flowRuntime`.
  </Accordion>

  <Accordion title="AI generation">
    Describe the experience in plain language and FirstFlow generates the flow graph, streamed live over SSE as nodes and edges appear. You then refine it manually or with the AI chat panel.
  </Accordion>

  <Accordion title="Import from URL">
    Point at a help article or feature page; FirstFlow fetches it and extracts the content into a flow you can edit.
  </Accordion>
</AccordionGroup>

## Status and lifecycle

An experience moves through three statuses. Only `active` experiences are eligible to be shown.

| Status   | Meaning                                                |
| -------- | ------------------------------------------------------ |
| `draft`  | Editable, not live. The default for new experiences.   |
| `active` | Live eligible to be shown to matching users.           |
| `paused` | Temporarily disabled; settings and flow are preserved. |

## Specific cases and limits

* **Eligibility is server-side.** Triggers, audience, schedule, frequency, and the classifier are all evaluated in `ConversationRouterService`. The browser cannot force a widget to appear it can only emit activity (a message or `notifyActivity()`) and let the server decide.
* **Type is immutable.** Pick `guide`, `tour`, `survey`, or `announcement` at creation. To change type, create a new experience.
* **Guides are AI-composed; the others are authored.** A guide's widget content is generated at runtime by Anthropic from the `result` instruction, so it varies with conversation context. Tour/survey/announcement steps render the nodes you authored, deterministically.
* **You edit `flow`, never `flowRuntime`.** The runtime graph is derived on save. If you save an unsupported graph shape, the chrome-stripping in `flowEditorToRuntime()` keeps the rest runnable rather than failing the whole flow.
* **Survey responses are analytics-only.** A completed survey emits a `survey_completed` event to analytics; there is no transactional response store. See [Analytics](/features/analytics).
* **Frequency is per user.** `once` and `limited` caps are tracked per user over the chosen `period`, so a user who already saw a once-only experience will not see it again.

## Next

Build the graph in [Flows & nodes](/concepts/flows-and-nodes), tune *when* and *to whom* in [Triggers & audience](/concepts/triggers-and-audience), and see what gets rendered in [Widgets & blocks](/concepts/widgets-and-blocks).
