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

# Audience & segments

> The end-user directory FirstFlow builds from your identity calls, and the precomputed segments that gate which experiences each user sees.

Every user who interacts with your agent becomes a row in the **audience** a
directory FirstFlow assembles from the identity you send. **Segments** are named
subsets of that directory, and an [experience](/concepts/experiences) can target
one so that only matching users are eligible to see it.

Targeting is evaluated server-side: when a user qualifies for an experience,
FirstFlow checks the experience's audience setting against that user's segment
membership before composing and pushing a widget. The browser cannot override
this segment membership is computed from data the backend already holds.

## 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 browser
[`user`](/react/identity) prop on `FirstflowProvider`, and the server
[`identify()`](/server/analytics) 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, `sessions`
  greater than `5`). 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_segment` flow action, or by the dashboard's "Add to segment"
  affordance on the audience list.

The crucial mechanism is that **membership is precomputed, not evaluated at
delivery time.** FirstFlow stores each user's segment IDs in an array on their
audience row (`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`](/concepts/triggers-and-audience) finds experiences
whose [trigger](/concepts/triggers-and-audience) 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](/react/realtime).

```mermaid theme={null}
flowchart TD
  A[Observed message] --> B{Trigger matches?}
  B -- no --> X[Skip experience]
  B -- yes --> C{Audience gate}
  C -->|type: all| D{Frequency cap OK?}
  C -->|type: segment| E{user in<br/>segments array?}
  E -- yes --> D
  E -- no --> X
  D -- no --> X
  D -- yes --> F{Classifier allows?}
  F -- no --> X
  F -- yes --> G[Compose widget<br/>claude-sonnet-4-6]
  G --> H[Push over Socket.IO]

  classDef gate fill:#f5e9ff,stroke:#7c3aed,color:#3b0764;
  classDef terminal fill:#e7f9ee,stroke:#16a34a,color:#064e3b;
  classDef stop fill:#fdeaea,stroke:#dc2626,color:#7f1d1d;
  class B,C,D,E,F gate;
  class G,H terminal;
  class X stop;
```

## The audience gate

An experience stores its targeting in `settings.audience`, a small object with
two fields:

```ts theme={null}
audience: {
  type: "all" | "segment";
  segment_id?: string; // a segment UUID, required when type is "segment"
}
```

When `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.

A deleted segment is handled at the data layer: deleting a segment purges its ID
from every member's `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:

| Field          | Maps to       | Typical operators                          |
| -------------- | ------------- | ------------------------------------------ |
| `name`         | member name   | `is`, `is_not`, `contains`, `not_contains` |
| `email`        | member email  | `is`, `is_not`, `contains`, `not_contains` |
| `sessions`     | session count | `gt`, `lt`, `gte`, `lte`                   |
| `lastActivity` | last seen     | `before`, `after`                          |
| `createdAt`    | first seen    | `before`, `after`                          |

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:

```ts theme={null}
import { createSegment } from "@/lib/api";

// "Power users" anyone with more than five sessions.
const segment = await createSegment("agt_…", {
  name: "Power users",
  description: "Active accounts we can show advanced guides to",
  filterRules: [{ field: "sessions", operator: "gt", value: "5" }],
});

// segment.id is the UUID you pass as settings.audience.segment_id
```

To target an experience at it, set the experience's audience to that segment in
the dashboard sidebar, or write the setting directly:

```ts theme={null}
import { updateExperience } from "@/lib/api";

await updateExperience("agt_…", "exp_…", {
  settings: {
    // …existing trigger, schedule, frequency, device…
    audience: { type: "segment", segment_id: segment.id },
  },
});
```

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

```tsx theme={null}
// Browser identify and attach traits on the provider.
<FirstflowProvider
  agentId="agt_…"
  publishableKey="pk_live_…"
  user={{ id: user.id, email: user.email, traits: { plan: "pro", role: "admin" } }}
>
  {children}
</FirstflowProvider>
```

```ts theme={null}
// Server same user, identified from your backend. Reads FIRSTFLOW_API_KEY.
import { identify } from "@firstflow/sdk";

identify({
  firstflowAgentId: "agt_…",
  userId: user.id,
  traits: { plan: "pro", role: "admin" },
});
```

The browser `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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

## Next

Segments share their condition vocabulary with [triggers and
audience](/concepts/triggers-and-audience), and the data they filter comes from
the [identity](/react/identity) you send from the browser and the
[server SDK](/server/analytics). To see how a qualifying user turns into a
delivered widget, read [experiences](/concepts/experiences).
