Skip to main content
By the end of this guide your AI product renders FirstFlow experiences in the browser and reports every LLM turn from your backend joined by a single conversation id you own. The browser SDK (@firstflow/react) renders whatever the backend pushes; the server SDK (@firstflow/sdk) observes your model calls so the backend can decide what to push. You need three things: an agent’s keys, the two packages installed, and one stable id passed to both sides.
Want the architecture first? How it works explains the server-side eligibility model in two minutes. This page is the hands-on path.

How the pieces connect

FirstFlow is server-authoritative. Eligibility triggers, audience, schedule, frequency, and any classifier is evaluated on the backend inside ConversationRouterService. When a user qualifies, the backend composes a WidgetTree (Anthropic claude-sonnet-4-6) and pushes the finished widget down a Socket.IO connection. The browser SDK is a passive consumer: it opens the socket, renders what arrives, and reports activity back. You cannot trigger a widget purely from the client your server’s observed LLM calls are what feed the trigger engine. That is why both SDKs matter, and why they must agree on one identifier. The browser passes it as conversationId; the server passes the same string as sessionId. That shared id is what links the widget, the realtime socket, the stored transcript, the branch-decision classifier, and any flow run into a single conversation. Get this one value consistent and everything downstream just works. The dotted edge is the only thing the browser decides on its own: it emits the user’s intent (flow.next, flow.back). The backend’s FlowNavigationListener advances the flow and composes the next tree. Navigation is server-side, just like eligibility.

Get your keys

Each agent is one workspace for one AI product. Create one in the dashboard, then open Settings → SDK integration. You will use three values: the agent ID and publishable key (pk_live_…) in the browser, and the API key (FIRSTFLOW_API_KEY) on the server. The first two are browser-safe; the publishable key is designed to ship in client code. The API key is a secret keep it server-side only.

Render experiences (browser SDK)

1

Install @firstflow/react

Styles auto-inject there is no CSS file to import. On Next.js, install @firstflow/nextjs instead; it re-exports the React surface plus server entry points.
2

Mount the provider and widget

Wrap your app once with FirstflowProvider and mount FirstflowWidget next to your chat. The provider opens the realtime socket and identifies the user; the widget renders whatever the backend pushes.
The user prop carries your already-authenticated user { id, email?, name?, traits? }. FirstFlow trusts this value; your auth provider is responsible for verifying who the user is. Traits flow into audience targeting and analytics. The provider throws if agentId or publishableKey is empty.
3

Mint the conversation id lazily

conversationId is optional and lazy you own it. Leave it empty until the conversation actually begins (the first message), then mint a stable id, persist it, and reuse it across reloads.
With no conversation yet, the socket connects on user identity alone, so chat_opens experiences (tours, surveys, announcements) still run in user scope. The moment a real id appears, the socket reconnects bound to it. Use a fresh id to start a new conversation. Full rules are in Conversations & sessions.

Observe LLM calls (server SDK)

The backend can only decide what to show if it sees the conversation. The server SDK gives you a drop-in client that mirrors the OpenAI / Anthropic SDK exactly, plus three required tags on each call.
1

Install @firstflow/sdk and set the secret

.env (server-only)
The SDK reads FIRSTFLOW_API_KEY from the environment never hard-code it, never expose it to the browser. Self-hosting? Also set FIRSTFLOW_API_BASE_URL to your backend.
2

Swap your client and tag every call

Import the pre-wrapped client from the peer-isolated subpath for your provider, then add firstflowAgentId, sessionId, and userId to each request body.
The wrapper is a transparent proxy. It instruments chat.completions.create, messages.create, responses.create, and embeddings.create, strips the three FirstFlow fields from the body before the request reaches OpenAI or Anthropic, and fires fire-and-forget observe hooks with model, tokens, latency, and finish reason once the call completes. Streaming is handled automatically for OpenAI it injects stream_options: { include_usage: true } so the final chunk carries token counts.
All three tags are required together. The wrapper never infers identity: tag a call with some but not all of them and the call is passed through unobserved plus a one-time console warning your conversation silently never reaches the trigger engine. The most common mistake is a sessionId that does not match the browser’s conversationId; when that happens the widget and the transcript end up in two different conversations.
Partial tagging is silent. If a request carries firstflowAgentId and userId but no sessionId, FirstFlow strips the fields, runs the model call normally, and observes nothing you only get a single warning the first time. Always pass all three, and confirm sessionId === conversationId.
If you cannot use the pre-wrapped clients (Vercel AI SDK, a custom gateway), call observe() directly with the same { firstflowAgentId, sessionId, userId } shape. The full server surface wrapClient, outcome, track, trace is in the Server SDK overview.

Create and publish an experience

With both SDKs wired, build something for the backend to push.
1

Create the experience

In your agent, go to Experiences → New experience and pick a type: guide, tour, survey, or announcement. The type is immutable after creation. A guide is a single AI-composed widget driven by a flow graph; the others are multi-step authored experiences. See Experiences.
2

Build the flow

Use the visual builder, or describe the flow and let AI generate it. The graph is { nodes, edges }; the backend walks it to a Result node and composes the widget. See Flows & nodes.
3

Set triggers and audience

Choose when it fires (e.g. after_user_message, chat_opens, or a classifier) and who sees it (all users or a segment). This is the server-side eligibility you saw above. See Triggers & audience.
4

Publish

Set status to Active. The next matching conversation receives it.

Verify it works

Open your app and start a conversation so the server SDK observes a turn. Two signals tell you the loop is closed:
  • The widget appears. A matching trigger fires, the backend composes the WidgetTree, and FirstflowWidget renders it. If nothing shows, the eligibility rules did not match that decision is server-side, so check the experience’s triggers and audience, not the browser.
  • The conversation is observed. In the dashboard, the experience’s analytics show the session and turns. If they are empty, the LLM call was likely tagged incompletely re-check that all three server tags are present and that sessionId equals the browser’s conversationId.
A useful sanity check: open your browser console after mounting the provider. A failed realtime connection logs [Firstflow] realtime connect failed:, which usually means a wrong publishableKey, agentId, or (when self-hosting) apiUrl.

Next

Go deeper on the side you’ll touch next: the Browser SDK for the widget, identity, commands, and theming, or the Server SDK for outcomes, traces, and configuration. The shared model behind both lives in Core concepts, and you can run the whole platform yourself via Self-hosting.

Browser SDK

Provider, widget, identity, commands, theming.

Server SDK

Wrapping clients, traces, outcomes, analytics.