Skip to main content
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; for the targeting rules see Triggers & 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. 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 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. The steps in order:
1

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

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

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

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

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}.
6

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.

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.
flow
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.
flowRuntime
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.
settings
ExperienceSettings
Triggers, audience, device, schedule, frequency, priority, placement, and presentation. Zod-validated server-side; see the fields below.
widgetTree
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.

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.
triggers
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.
audience
{ type: 'all' | 'segment'; segment_id?: string }
default:"{ type: 'all' }"
Who is eligible every user, or a named segment. A deleted segment falls back to all.
device
'all' | 'desktop' | 'mobile'
required
Restricts which devices can receive the experience.
schedule
{ 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.
frequency
{ 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).
priority
number
default:"0"
Tie-breaker when multiple experiences are eligible for the same trigger. Higher wins.
entry_points
('slot' | 'slash')[]
default:"['slot']"
Where the experience can surface: in the widget slot, or as a slash command the user invokes. See Commands.
presentation
{ 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.

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 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.
  • 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.
  • Slack Message posts a channel message or DM. Config: slackActionType, slackChannelId, slackMessageText, slackContinueOnFailure. See Slack.
  • UI Widget composes a custom widget from primitives, carrying an authored WidgetTree (schema v2) directly. See Widgets & 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

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.
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.
Point at a help article or feature page; FirstFlow fetches it and extracts the content into a flow you can edit.

Status and lifecycle

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

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.
  • 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, tune when and to whom in Triggers & audience, and see what gets rendered in Widgets & blocks.