# Targeting flows

Who enters an experiment, and with which split, can depend on country, device, browser, UTM parameters, cookies, time, the app's platform and version, or whether the visitor is new or returning. In SplitHub this is a small node graph, the **flow**, on the experiment.

## In the dashboard

Open an experiment → Targeting. The editor has three node types:

- **Condition**: one or more conditions, combined with `match: 'all'` or `'any'`. Fields include `country`, `region`, `city`, `device`, `browser`, `os`, `language`, `referrer`, `url`, `path`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `query`, `cookie`, `visitor`, `hour`, `weekday`, `platform`, `app_version`. For `query` and `cookie`, set `key` to the parameter or cookie name. Operators: `is`, `is_not`, `in`, `not_in`, `contains`, `not_contains`, `starts_with`, `matches`, `exists`, `not_exists`, plus `gte` (at least) and `lt` (below) for `app_version` and `hour`. Values are strings; `in` uses a comma-separated list.
- **Split**: weighted outputs; each output goes to a variant or to another node.
- **Variant** / **Skip**: the leaf. `Skip` means "not in the experiment": the visitor sees the control and no exposure is recorded.

A toggle switches to a **simple list** view (sections top to bottom, drag to reorder) for flows that are just "if country in EU → split A, else → split B". While the flow editor is open, variant editing is locked so the two cannot drift; on save the editor collapses to a one-line summary.

## In the config

The same graph serializes as `flow` on the experiment and comes into `splithub.config.ts` with `npx splithub pull`:

```ts
'hero-title': {
  kind: 'value',
  status: 'running',
  variants: { control: { weight: 1, value: 'A' }, bold: { weight: 1, value: 'B' } },
  flow: {
    start: 'geo',
    nodes: {
      geo:   { type: 'condition', conditions: [{ field: 'country', op: 'in', value: 'DE,AT,CH' }], then: 'eu', else: 'rest' },
      eu:    { type: 'split', outputs: [{ weight: 50, to: 'control' }, { weight: 50, to: 'bold' }] },
      rest:  { type: 'split', outputs: [{ weight: 90, to: 'control' }, { weight: 10, to: 'bold' }] },
      control: { type: 'variant', variant: 'control' },
      bold: { type: 'variant', variant: 'bold' },
    },
  },
}
```

`defineConfig` validates the flow: node references, supported fields and operators, split weights, and declared variant names are checked. Design each path to end in a variant or skip; avoid cycles.

## Apps: platform and version

`platform` is `ios`, `android`, `macos`, `windows`, `linux`, or `web` for a browser. `app_version` is the app's version (`2.4.1`) and is empty on websites. Versions compare part by part, so `2.10.0` is at least `2.9`:

```ts
conditions: [{ field: 'app_version', op: 'gte', value: '2.4' }]   // the dashboard reads it as "App version at least 2.4"
```

Gate an app experiment on the first version that ships its variant code; older builds see the control and are not counted. See [Apps](/docs/apps).

## Where it runs

Everywhere the assignment runs, with the same answer:

- the React SDK (browser context: device, browser, path, query, cookies, time in the visitor's time zone; country, region and city from the cloud's `ctx` when in cloud mode);
- the Next.js middleware (request headers and Cloudflare/Vercel geo);
- the drop-in script `sh.js`, which walks the same flow as the SDK;
- the React Native, iOS and Android SDKs (platform, app version, language, device; country from the cloud);
- server code using `assign(config, experimentId, visitorId, { context })`.

Pass supported context fields with `createClient(config, { context: { country: 'DE' } })` or the provider's `context` prop. For a plan stored in a cookie, use `{ field: 'cookie', key: 'plan', op: 'is', value: 'pro' }`.

## Order of checks

For each visitor and experiment the SDK, the middleware and `sh.js` decide in the same order:

1. a `?sh_force=<id>:<variant>` preview wins and records nothing;
2. crawlers see the control and are never counted;
3. the experiment must be running and inside its schedule;
4. a visitor in the [holdout](/docs/config) sees the control (reason `holdout`);
5. a visitor outside the experiment's [layer](/docs/config) range sees the control (reason `layer`);
6. the `traffic` share, then the flow: a `Skip` leaf gives the control without an exposure (reason `targeting`).

So a flow only ever sees the visitors the holdout and the layer let in. `?sh_debug=1` shows the reason for each decision.

## When to use it

- Roll a change out to one market first.
- Keep a mobile-only variant out of desktop traffic.
- Test only paid traffic (`utm_medium is cpc`).
- Exclude employees by cookie.
- Run an app experiment only on the versions that have its code.

Remember the statistics: targeting narrows the sample. The verdict is about the visitors who entered, not about everyone. When conditions decide which variant a visitor reaches, the expected split depends on who visits, so the split check (SRM) is skipped and the verdict notes say so.
