Skip to main content
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 is a workspace that holds experiences; each experience has a flow, triggers, and an audience; the backend evaluates eligibility against a live conversation and pushes a widget 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.

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; 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. 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 is the browser runtime. Mount FirstflowProvider once with agentId and a publishableKey (the browser-safe pk_live_… key not a secret), render FirstflowWidget where the widget should appear, and call useFirstflow() for identity, analytics, commands, and notifyActivity(). It opens the realtime socket and consumes widget.show pushes. @firstflow/sdk is the server runtime. It reads FIRSTFLOW_API_KEY (the secret server key) from the environment and wraps your 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 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 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: 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 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 and Audience.

Conversations, sessions, and analytics

You own the conversation id. Pass it to FirstflowProvider 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. 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.

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 and @firstflow/widget-kit.

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.

Next

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

Agents

How it works