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

# Core concepts

> One model spans the dashboard, the backend, and the SDKs: agents hold experiences, the server decides what to show, and a widget is pushed to the browser.

FirstFlow has a single domain model that runs through the dashboard you author in, the backend that decides what to show, and the SDKs you install in your product. Learn it once and the same vocabulary applies whether you are dragging nodes in the flow editor or reading data back through `@firstflow/sdk`.

The shortest version: an [agent](/concepts/agents) is a workspace that holds [experiences](/concepts/experiences); each experience has a [flow](/concepts/flows-and-nodes), [triggers, and an audience](/concepts/triggers-and-audience); the backend evaluates eligibility against a live [conversation](/concepts/conversations-and-sessions) and pushes a [widget](/concepts/widgets-and-blocks) to the browser. Everything below expands that sentence.

## The mental model

Hold five nouns in your head and the rest follows.

An **agent** is the top-level container one per AI product or environment. It groups experiences, the audience, settings, and the conversations that flow through it. In code it is `agentId` in the browser and `firstflowAgentId` when you tag a server-side LLM call. It is never called an "app".

An **experience** is one deliverable inside an agent. It has a `type` chosen at creation and immutable thereafter: `guide` (a single AI-composed widget driven by a flow graph), or `tour`, `survey`, and `announcement` (multi-step authored sequences). Each experience carries its own flow, triggers, audience, schedule, and frequency.

A **flow** is the node graph that defines an experience messages, questions, branches, side-effects, and (for guides) a terminal Result node. Each **node** is one step: a `trigger`, a `classify` branch, an `api_call`, a `webhook`, a `result`, and so on. The flow editor saves React Flow's `{ nodes, edges, viewport }` to the `flow` field; the backend derives a stripped-down `flowRuntime` from it for execution.

A **conversation** (also called a **session**) is one chat thread. You own its id: it is `conversationId` in the browser SDK and `sessionId` on the server, and the two must be the same string for the server's observed messages to line up with the browser that renders the widget.

A **widget** is the rendered output a `WidgetTree` of UI block primitives (text, buttons, inputs, ratings, checklists) that the browser SDK mounts. The tree schema lives in [`@firstflow/widget-kit`](/widget-kit/overview).

## How it works end to end

Eligibility is decided server-side, not in the browser. Your product wraps its LLM client with the [server SDK](/server/overview); every observed completion is reported to the FirstFlow backend tagged with `firstflowAgentId`, `sessionId`, and `userId`. The backend matches the message against each experience's triggers and audience, optionally runs a classifier, walks the flow to a result, composes a `WidgetTree` with Anthropic (`claude-sonnet-4-6`), and **pushes it over Socket.IO** to the exact user. The browser SDK is a passive consumer: it renders what arrives and reports activity back. That is the North Star you cannot trigger a widget purely from the client; you observe and identify, and the server decides.

```mermaid theme={null}
flowchart TD
    A["Host app LLM chat turn"] -->|"wrapClient intercepts"| B["@firstflow/sdk<br/>tags firstflowAgentId + sessionId + userId"]
    B -->|"POST /v1/conversations/:id/messages (202)"| C{"Backend<br/>ConversationRouterService"}
    C --> D["1. Resolve trigger from message role"]
    D --> E["2. Filter eligible experiences<br/>(triggers + audience)"]
    E --> F["3. Classifier / branch decision<br/>(LLM gate, if configured)"]
    F --> G["4. Walk flow graph to Result node"]
    G --> H["5. Compose WidgetTree<br/>Anthropic claude-sonnet-4-6"]
    H -->|"Socket.IO widget.show<br/>room member:{ws}:{user}"| I["@firstflow/react<br/>renders WidgetTree → DOM"]
    I -->|"widget.action: flow.next / flow.back"| C

    classDef host fill:#fde68a,stroke:#b45309,color:#1f2937;
    classDef sdk fill:#bbf7d0,stroke:#15803d,color:#1f2937;
    classDef server fill:#ddd6fe,stroke:#6d28d9,color:#1f2937;
    classDef step fill:#f1f5f9,stroke:#64748b,color:#1f2937;
    class A host;
    class B,I sdk;
    class C server;
    class D,E,F,G,H step;
```

Two consequences are worth internalizing. First, the message ingest endpoint returns `202` and processes fire-and-forget, so observing a turn never blocks your chat response. Second, multi-step navigation (`flow.next` / `flow.back`) is also server-side: the browser emits the intent over the socket, the backend advances the flow step, fires any deferred action nodes, and pushes the next tree.

## Agents, experiences, and the SDK boundary

The split between what runs in the browser and what runs on your server maps cleanly onto the two main SDKs. The browser SDK renders and reports; the server SDK observes and decides-the-inputs.

[`@firstflow/react`](/react/overview) is the browser runtime. Mount [`FirstflowProvider`](/react/provider) once with `agentId` and a `publishableKey` (the browser-safe `pk_live_…` key not a secret), render [`FirstflowWidget`](/react/widget) where the widget should appear, and call [`useFirstflow()`](/react/use-firstflow) for identity, analytics, [commands](/react/commands), and `notifyActivity()`. It opens the realtime socket and consumes `widget.show` pushes.

[`@firstflow/sdk`](/server/overview) is the server runtime. It reads `FIRSTFLOW_API_KEY` (the secret server key) from the environment and [wraps your LLM client](/server/wrap-llm-client) so every `chat.completions.create`, `messages.create`, `responses.create`, or `embeddings.create` is observed. Subpaths target each provider: `@firstflow/sdk/openai`, `/anthropic`, `/aiclient` (any OpenAI-compatible endpoint Ollama, vLLM, LM Studio), and `/langchain`. Every observed call must be tagged with all three of `firstflowAgentId`, `sessionId`, and `userId`; partial tagging means the request passes through unobserved and you get a one-time warning.

[`@firstflow/nextjs`](/react/nextjs) re-exports the React client and exposes the server SDK under `/server`, `/server/openai`, `/server/anthropic`, and `/server/aiclient`, so a single dependency covers both halves of an App Router project. [`@firstflow/widget-kit`](/widget-kit/overview) holds the shared `WidgetTree` / `Block` schema and primitives.

## Flows and nodes

A flow's runtime graph is `{ nodes, edges }` where each node carries `data.config`. Authoring node types come from a single registry in the dashboard, and which nodes are available depends on the experience type. The universal flow-level nodes are:

| Node            | What it does at runtime                                                                                                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `trigger`       | Auto-created entry point; holds the `triggerType` (e.g. `chat_open`).                                                                                                                                                    |
| `classify`      | LLM branch routes by classification via the `branch-decisions` provider (default `openai` / `gpt-4o-mini`, with `confidenceFloor`, `fallbackBranchId`, `resolutionMode` `blocking`/`optimistic`, `ttlMs`, `signalMask`). |
| `api_call`      | Server-side HTTP GET; the response feeds downstream nodes (`apiCallUrl`, `apiCallTimeoutMs`, `apiCallMaxResponseBytes`).                                                                                                 |
| `webhook`       | Outbound HTTP request; can run inline or be deferred until `flow.next` (`webhookUrl`, `webhookMethod`, HMAC-signed when `webhookSecret` is set).                                                                         |
| `slack_message` | Posts a channel message or DM via the [Slack integration](/integrations/slack).                                                                                                                                          |
| `result`        | Terminal node (guides) holding the instruction the widget is composed from.                                                                                                                                              |
| `widget`        | A custom widget composed directly from `@firstflow/widget-kit` primitives.                                                                                                                                               |

For a guide, the backend's flow walker traverses from the trigger through any decisions and action nodes to a single Result node, then hands its instruction to widget composition. Branch conditions use AND within a branch and OR across branches, and they evaluate against `traits.*` and `answers.*`. See [Flows and nodes](/concepts/flows-and-nodes) for the full node reference.

## Triggers, audience, and eligibility

Triggers and audience are the eligibility rules the backend evaluates before composing anything. They live in the experience's Zod-validated `settings`. Each row in `settings.triggers[]` has a `when.show_on` event one of `chat_opens`, `after_user_message`, `after_user_and_agent_message`, `after_idle` (with `delay_seconds`), `custom_event`, or `conversation_classifier` plus optional `conditions[]` and `actions[]`.

Conditions target `traits.*` or `answers.*` using the canonical operators `equals`, `not_equals`, `contains`, `not_contains`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `exists`, and `not_exists`. Audience is either `all` or a named `segment` (a group defined by trait rules). The same `settings` also carry `schedule`, `frequency` (`once` / `limited` / `always` over a `session` / `day` / `week` / `lifetime` window), `priority`, and `device`. Because all of this runs server-side, the browser cannot force a widget to appear. See [Triggers and audience](/concepts/triggers-and-audience) and [Audience](/concepts/audience).

## Conversations, sessions, and analytics

You own the conversation id. Pass it to [`FirstflowProvider`](/react/provider) as `conversationId` (optional and lazy mint and persist a stable id on the first message) and to the server SDK as `sessionId`, using the same string in both places. That shared id is how an observed server-side message and the browser that renders the resulting widget refer to the same thread. See [Conversations and sessions](/concepts/conversations-and-sessions).

Analytics flow through the same plumbing. The browser SDK batches `track` / `identify` / `page` events over the existing socket; the backend validates them against its event catalog and writes to ClickHouse. Survey and form answers are analytics-only there is no separate transactional response store. Conversation analytics and session KPIs are surfaced under [Analytics](/features/analytics).

## Widgets and blocks

The unit the browser renders is a `WidgetTree`: a `schemaVersion`, a `widgetId`, a `header`, a single root `block`, and optional `initialState` and (schema v2 guides) `slots`. Blocks compose recursively layout primitives (`stack`, `row`, `card`, `carousel`), content (`title`, `text`, `media`, `badge`, `divider`), and interactive primitives (`button`, `pill_group`, `text_input`, `slider`, `checkbox`, `rating`, `checklist`). Interactive blocks bind to state via a `Ref` like `{ ref: "slots.name" }` and dispatch actions; only one terminal action (`flow.next`, `flow.back`, `submit`, `widget.dismiss`, `tree.replace`) may run, and it always runs last. The full primitive catalog is at [Widgets and blocks](/concepts/widgets-and-blocks) and [`@firstflow/widget-kit`](/widget-kit/overview).

## Keys and terminology

Two keys, two scopes. The **publishable key** (`pk_live_…`) is browser-safe and used by `@firstflow/react` it is not a secret. The **API key** (`FIRSTFLOW_API_KEY`) is the secret server key used by `@firstflow/sdk`; keep it server-only. For self-hosting, override the base URLs (`apiUrl` in the browser, `FIRSTFLOW_API_BASE_URL` on the server); both default to `https://api.firstflow.app`.

Throughout the docs, the workspace container is always an "agent" (`agentId`), never an "app". "AI agent products" refers to the product category you are building, and "coding agent" refers to a tool like Claude Code or Cursor talking to FirstFlow over [MCP](/mcp/overview).

## Next

Start with the container, then read how it decides what to show.

<CardGroup cols={2}>
  <Card title="Agents" icon="cube" href="/concepts/agents" />

  <Card title="How it works" icon="diagram-project" href="/how-it-works" />
</CardGroup>
