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

# Targeting

> How FirstFlow decides who sees an experience and when triggers, audience, schedule, frequency, and the LLM classifier, all evaluated server-side.

Targeting is the set of rules that decide **which** experience shows, **to whom**, and **when**. Every dimension the trigger event, the audience, the schedule window, the frequency cap, and the optional LLM classifier is evaluated on the backend by `ConversationRouterService`. The browser SDK never makes this decision: it reports activity and identity, the server resolves eligibility, composes the widget, and pushes the finished result over Socket.IO. This page is the map of the targeting model; each dimension links to the concept page where the field semantics live.

<Note>
  Because eligibility is server-side, the browser cannot force a widget to show. You call [`notifyActivity()`](/react/use-firstflow) or send a message; the server decides whether anything appears. That is also why targeting only works when you feed it accurate identity and traits see [Feed it data](#feed-it-data) below.
</Note>

## How targeting is evaluated

When the [server SDK](/server/overview) observes a message, it posts the turn to the backend, which runs an ordered pipeline. The trigger type is resolved from the message role first; then [audience](/concepts/audience) narrows by trait rules; then the [classifier](/concepts/triggers-and-audience#classifier-triggers) (if configured) acts as an LLM gate; then schedule and frequency caps apply; and finally, among everything still eligible, the highest-priority experience wins. Only then is the widget composed (Anthropic `claude-sonnet-4-6`) and pushed to the browser.

The dimensions are filters applied in sequence an experience must pass **all** of them to be shown. Reading the diagram top-to-bottom is the order they run in.

```mermaid theme={null}
flowchart TD
  A([Message observed by server SDK]) --> B{Trigger matches?<br/>chat_open · after_user_message · after_idle · custom · classifier}
  B -- no --> X([Nothing shown])
  B -- yes --> C{In audience?<br/>all or segment trait rules}
  C -- no --> X
  C -- yes --> D{Classifier gate?<br/>LLM confidence ≥ floor}
  D -- below floor --> X
  D -- pass / not configured --> E{Schedule + frequency?<br/>within window · cap not hit}
  E -- no --> X
  E -- yes --> F[Pick highest priority among eligible]
  F --> G[Compose WidgetTree<br/>claude-sonnet-4-6]
  G --> H([Push over Socket.IO → browser renders])

  classDef pass fill:#dcfce7,stroke:#16a34a,color:#14532d;
  classDef stop fill:#fee2e2,stroke:#dc2626,color:#7f1d1d;
  classDef work fill:#fef9c3,stroke:#ca8a04,color:#713f12;
  class A,H pass;
  class X stop;
  class F,G work;
```

## Triggers the event that makes an experience eligible

A trigger is the event that opens the window for an experience. The available triggers depend on the experience type: `chat_open` fires when the chat opens, `after_user_message` after the user sends a message, `after_idle` after a configured number of seconds of inactivity, and `on_custom_event` when your app emits a named event. Guides additionally support `after_user_and_agent_message` (after the user message **and** the agent's reply). The idle timer is driven from the browser call [`notifyActivity()`](/react/use-firstflow) on keystroke, click, or send to reset it, otherwise `after_idle` will fire too eagerly.

The most powerful trigger is `conversation_classifier`, which runs an LLM gate before showing the experience. You configure custom instructions, the provider and model, a confidence floor, a TTL, a resolution mode, the context window (how many turns the model sees), and signal masking (which data it sees). Use it for intent-driven experiences like "offer help when the user seems stuck" something no static rule can express. The full trigger table and classifier fields are documented in [Triggers & audience](/concepts/triggers-and-audience).

## Conditions & operators gate on traits and prior answers

Triggers can carry conditions that test user **traits** (`traits.*`) or prior survey **answers** (`answers.<questionId>`). Conditions are how you combine "this event happened" with "and this is true about the user," for example: fire `after_idle` only when `traits.plan` equals `free`. The same operator set powers both trigger conditions and [segment](/concepts/audience) rules, so a rule you learn once applies everywhere.

| Operator                    | Meaning                      |
| --------------------------- | ---------------------------- |
| `equals` / `not_equals`     | Exact (in)equality.          |
| `contains` / `not_contains` | Substring or set membership. |
| `in` / `not_in`             | Value is (not) in a set.     |
| `gt` / `gte` / `lt` / `lte` | Numeric comparisons.         |
| `exists` / `not_exists`     | Field is present or absent.  |

## Audience who is eligible

Audience targeting decides **who** can see an experience. The two modes are **all** (every user) and **segment** (only users in a named [segment](/concepts/audience)). A segment is a saved set of trait rules using the operators above computed by the backend against the audience directory. If a segment referenced by an experience is later deleted, targeting falls back to "all users" rather than showing nothing, so a deleted segment never silently hides an experience.

Set an experience's audience to a segment and only matching users qualify. The audience directory itself names, emails, traits, tags, session counts, and segment membership is populated entirely from the identity you send, which is why the [Feed it data](#feed-it-data) section below matters as much as the rules themselves.

## Schedule, frequency, priority & placement

Beyond who and when, four settings shape delivery. **Schedule** controls the active window start now or at a date/time, with an optional expiration. **Frequency** caps how often a user sees an experience: `once`, `limited` (per session, day, week, or lifetime), or `always` (the default for guides). **Priority** breaks ties when more than one experience is eligible for the same turn, the higher priority wins and the rest are suppressed. **Device** narrows to all, desktop, or mobile, and **placement** controls where the widget renders (overlay, inline, above-composer, or full-page). These all live under the experience's settings; see [Triggers & audience](/concepts/triggers-and-audience#schedule-frequency--placement) for the per-mode detail.

## Feed it data

Targeting is only as good as the traits you send segment rules and trait conditions can only match fields that actually exist on the user. Set identity early (on login) and update it whenever traits change, from either side of the connection:

```tsx theme={null}
// browser identity is trusted; your auth provider verifies the user
<FirstflowProvider
  agentId="agt_…"
  publishableKey="pk_live_…"
  user={{ id: user.id, email: user.email, traits: { plan: "pro", role: "admin" } }}
>
  {children}
</FirstflowProvider>
```

```ts theme={null}
// server for traits you only know backend-side (fire-and-forget, reads FIRSTFLOW_API_KEY)
import { identify } from "@firstflow/sdk";

identify({
  firstflowAgentId: "agt_…",
  userId: user.id,
  traits: { plan: "pro", company_size: "50-200" },
});
```

Pass the same `userId` from both sides and the traits attribute to one user. The browser [`user`](/react/identity) prop and the server [`identify()`](/server/analytics) helper write to the same audience record; the merged traits are what audience segments and `traits.*` conditions evaluate against. You can also update traits at runtime from the instance with `ff.setUser({ id, traits })` see [Identity & traits](/react/identity).

## A worked example

Suppose you want to nudge free-plan users toward an upgrade guide, but only when they seem to be hitting limits. Combine the dimensions: a `conversation_classifier` trigger with the instruction "user is asking about usage limits or pricing" and a confidence floor; a **segment** audience whose rule is `traits.plan equals free`; a frequency cap of `limited` (once per day) so it never nags; and a high priority so it beats lower-value announcements competing for the same turn. The user types about hitting a quota, the server runs the classifier on the recent turns, confirms the segment match from the `plan` trait you sent, checks the daily cap, and if all pass composes and pushes the guide. None of that touches the browser until the finished widget arrives.

## Next

Author the rules on the [Triggers & audience](/concepts/triggers-and-audience) and [Audience & segments](/concepts/audience) concept pages, then make sure your [Identity & traits](/react/identity) integration is sending the fields you target on.
