How it works
The audience is not something you upload. FirstFlow writes a member row the first time it sees a user, then keeps it current from two sources: the browseruser prop on FirstflowProvider, and the server
identify() helper. The id you send becomes the row’s
stable externalUserId; the name, email, and traits become the fields you
can filter and segment on. Send more, target more precisely.
A segment is a saved definition that resolves to a set of those rows. FirstFlow
splits segments into two kinds by how membership is determined:
- A dynamic segment has one or more filter rules (for example,
sessionsgreater than5). FirstFlow evaluates the rules against the directory and maintains the matching set for you. - A manual segment has no rules. Membership is assigned explicitly by the
assign_segmentflow action, or by the dashboard’s “Add to segment” affordance on the audience list.
agent_audience.segments[]). SegmentMembershipService
recomputes that array whenever the user’s data changes (a new session,
lastSeenAt, traits) and whenever a segment’s rules change. Eligibility at
delivery is then a fast array-contains check no rules are re-run on the hot
path.
This is why targeting is reliable but eventually consistent: edit a dynamic
segment’s rules and the affected members are re-synced in the background, not
the instant a widget would fire.
Where audience fits in eligibility
Audience is one gate in a server-side sequence. When an observed message arrives,ConversationRouterService finds experiences
whose trigger matches, then for each candidate
checks audience, then frequency, then the optional LLM classifier. Only an
experience that clears every gate is composed (Anthropic claude-sonnet-4-6) and
pushed over Socket.IO.
The audience gate
An experience stores its targeting insettings.audience, a small object with
two fields:
type is all, every user passes the gate. When type is segment, the
backend loads the user’s audience row and checks whether segment_id is present
in their precomputed segments[] array. Present means eligible; absent means the
experience is skipped for that user.
Two fallbacks make the gate fail open rather than silently hide everything:
- Unknown user. If the user has no audience row yet common on a first visit before identity has propagated the gate passes conservatively. A brand-new user is not excluded just because their row hasn’t been written.
- Lookup error. If the membership lookup throws, the gate passes conservatively and logs a warning, so a transient database issue never blanket- suppresses experiences.
segments[]. An experience still pointing at that ID matches
nobody by membership, so targeting effectively reverts to “all users.” Re-point
the experience at a live segment to restore narrow targeting.
Defining segments
A segment’s rules are an array of{ field, operator, value } objects. Dynamic
membership is computed by translating those rules into a query over the audience
directory, so rules operate on the directory’s own columns:
Rules referencing any other field are skipped during membership evaluation, so a
rule on an arbitrary trait will not narrow a dynamic segment keep dynamic rules
to the fields above, and use a manual segment (assigned in a flow) for richer,
trait-driven grouping.
Create a segment through the typed API client; FirstFlow recomputes its user
count and schedules the background membership sync:
Keep the directory rich
Targeting is only as good as the traits you send, and segments can only filter on data that exists. Set identity early on login, and again whenever a trait changes so a user’s row is populated before any experience tries to target them.traits and server identify() traits both land on the same member
row, keyed by the id / userId you send. Use a stable id (your own user
primary key), not an email or session value, so updates accumulate on one row
instead of forking into duplicates.
Specific cases
A user qualifies but the widget doesn't appear
A user qualifies but the widget doesn't appear
Audience is one of several gates. Confirm the experience is
active, its
trigger matches the message, the frequency cap isn’t exhausted, and if
configured the classifier allows. Audience only decides segment
eligibility, not whether anything fires.I edited a dynamic segment's rules but counts look stale
I edited a dynamic segment's rules but counts look stale
Membership is precomputed and synced in the background after a rules change.
The cached
userCount is refreshed immediately from the new rules; the
per-user segments[] arrays are reconciled asynchronously. Targeting catches
up shortly after.A trait-based dynamic segment matches nobody
A trait-based dynamic segment matches nobody
Dynamic rules evaluate only over
name, email, sessions,
lastActivity, and createdAt. A rule on any other trait is skipped. For
trait-driven grouping, use a manual segment and assign members with the
assign_segment flow action or the dashboard’s “Add to segment” affordance.The targeted segment was deleted
The targeted segment was deleted
Deleting a segment removes its ID from every member’s
segments[]. An
experience still pointing at the dead ID matches nobody by membership, so it
effectively targets all users. Re-point it at a live segment to restore the
narrower audience.