---
title: Targeting and frequency
description: Choose trigger moments, audiences, saved audiences, stable sampling and how often a person is asked.
url: https://docs.userreason.com/prompts/targeting/
updated: 2026-10-16
---

# Targeting and frequency

A published survey combines a trigger with audience, sampling and frequency checks. Passing one check does not bypass the others. Start with [Your first survey](https://docs.userreason.com/get-started/first-prompt/index.md) before adding restrictions.

## Triggers

A survey has 1–32 triggers. Any matching trigger provides an opportunity to evaluate it.

| Type | JSON shape | When it runs |
| - | - | - |
| Application event | `{ "type": "event", "event_key": "onboarding.completed" }` | Your app calls the matching `event()`. Define the event on the site before publishing the trigger. |
| Page load | `{ "type": "page_load" }` | The initialized SDK observes the page, or your app calls `show()` for the selected survey. |
| Route | `{ "type": "route", "path_prefix": "/reports" }` | The current pathname starts with the prefix, initially or after an SPA route change. |
| Selector | `{ "type": "selector", "selector": "#export", "event": "click" }` | A matching element is clicked. Use `"visible"` for an element becoming visible instead. |
| Delay | `{ "type": "delay", "milliseconds": 30000 }` | The configured delay elapses on the current route. Range: 1–86400000 milliseconds. |
| Scroll | `{ "type": "scroll", "percent": 50 }` | Document scroll progress reaches the percentage, from 0 to 100. |

Delay, scroll and visibility triggers run once per route; route changes reset their observations and delay timers. Selector clicks can recur. A click is only a UI opportunity: report an application event after confirmed success when the action itself matters.

`automaticTriggers: false` disables the automatic observers. Explicit `event()` and `show()` still work. [show()](https://docs.userreason.com/sdk/show/index.md) supplies only a page-load opportunity, so it cannot trigger an event-only survey.

## Audiences

Use `always` for everyone, `all` or `any` to combine rules, and `not` to negate one rule. A field condition selects its source, key and comparison:

```json
{
  "type": "field",
  "source": "trait",
  "key": "plan",
  "comparison": { "operator": "eq", "value": "pro" }
}
```

| Source | Values |
| - | - |
| `trait` | Browser traits supplied with `identify()`. |
| `verified_trait` | Traits asserted by your backend through verified identity. |
| `event_property` | Properties of this application event. |
| `verified_event_property` | Properties verified for this occurrence through its backend proof. |
| `context` | Browser or server context, such as the current path. |
| `application_context` | Current unverified app facts provided through `setContext()`, kept separate from identity and server context. |

Comparisons are `eq` or `neq` with a scalar value; `in` or `not_in` with an array of scalar values; numeric `gt`, `gte`, `lt`, `lte`; and `exists` or `not_exists` without a value. Missing values are unknown for value comparisons, including `neq`; use `not_exists` when absence is intentional. Use the verified sources when eligibility depends on trusted account or business data.

For example, combine a plan with an event property:

```json
{
  "type": "all",
  "rules": [
    { "type": "field", "source": "trait", "key": "plan", "comparison": { "operator": "eq", "value": "pro" } },
    { "type": "field", "source": "event_property", "key": "format", "comparison": { "operator": "eq", "value": "csv" } }
  ]
}
```

## Local activity counts and delay

Use a local rule for frequent actions that should not be uploaded:

```json
{
  "type": "local_event",
  "version": 1,
  "event_key": "editor.selection_changed",
  "dimension_key": "",
  "minimum": 5,
  "delay_ms": 0
}
```

Combine it with the matching event trigger. `minimum` is the observed count in this browser; an optional `maximum` adds an upper bound. `delay_ms` waits from the first observed occurrence, from 0 to 86400000 milliseconds. While the app is unavailable, the SDK keeps a bounded pending opportunity and checks it again when availability returns.

This example counts selection changes in an editor; five is illustrative, not a recommended threshold. Choose a minimum, upper bound and delay for the product question. A narrow count window can expire while the app is busy, so use an upper bound only when later activity should exclude the person. Keep thresholds in survey configuration. Use an existing application counter through `application_context` when the requirement is account-wide or lifetime activity; local observations do not establish cross-device history. The [first-spins example](https://docs.userreason.com/examples/first-spins-feedback/index.md) explains that distinction for a game.

These conditions require a client supporting `event_policies:v1`. Older SDKs are not sent surveys that depend on them. The dashboard's **Check targeting**, or `userreason prompt validate --file PROMPT.json`, explains missing facts, collection policy and required client capabilities without publishing.

## Event history

History rules can use occurrence count, first occurrence or last occurrence for an event and dimension. Select `sdk` or `host_backend` as the source. These are distinct histories; a browser report does not become a backend occurrence just because it has the same name.

```json
{
  "type": "event_history",
  "event_key": "report.exported",
  "dimension_key": "reports",
  "source": "host_backend",
  "require_complete_history": true,
  "comparison": { "metric": "count", "operator": "gte", "value": "3" }
}
```

Counts are decimal **strings**, with operators `eq`, `gte` or `lte`. For `first_occurred_at` and `last_occurred_at`, use `before` or `after` with an ISO timestamp.

New history requires server collection. If collection is stopped, existing unexpired history remains available until its declared retention ends. Missing or expired history is not evidence that something never happened. `require_complete_history` prevents an incomplete history window from qualifying as a complete account history. The dimension must match the one supplied with the event; the default dimension is `""`.

## Previous surveys

A `prompt_activity` rule looks at what a person did with other surveys of the same site. Use it to avoid asking the same people twice, or to follow up with the people who answered.

```json
{
  "type": "not",
  "rule": {
    "type": "prompt_activity",
    "activity": "seen",
    "campaign_ids": ["4b2f6c1e-8a57-4d0e-9a3b-2f1c5d7e8a90"],
    "match": "any",
    "window": { "type": "last_days", "days": 30 },
    "scope": "all_enrollments"
  }
}
```

This leaves out anyone who saw that survey in the last 30 days. Without the surrounding `not`, the same rule would reach only those people.

| Field | Values |
| - | - |
| `activity` | `seen` (confirmed on the person's screen), `started`, `dismissed`, `declined` (they chose "Don't ask again"), `completed`, `screened_out`. |
| `campaign_ids` | 1–32 survey IDs of this site, each once. A survey may name itself. Archived surveys keep their history and can be named; an ID that is not on the site stops publishing with `prompt_activity_unknown_prompt`. |
| `match` | `any`: the activity on at least one of the surveys. `all`: on every one of them. |
| `window` | `{ "type": "ever" }`, `{ "type": "last_days", "days": 30 }` (1–3650), or `{ "type": "since", "at": "2026-09-01T00:00:00.000Z" }`. |
| `scope` | `all_enrollments` counts every enrollment of each survey. `current_enrollment` ignores runs before its current `enrollment_epoch`. |

**Seen is not the same as let through.** A survey counts as seen when its display was confirmed on the person's screen, or when something proves it was there: they started it, dismissed it or answered it. When a survey was let through inside the window and nothing confirmed it, nobody knows whether the person saw it. That condition is then unknown, and an unknown condition never passes, inside `not` either. Such a person is left out of both "has seen" and "has not seen", and their eligibility in the dashboard says "Not enough information".

**History has a beginning.** A site's record starts with its first confirmed display. "Ever" means since then, and a window that reaches further back cannot see anything earlier. The editor shows the date.

**The server decides.** What a visitor did with other surveys is not sent to the browser. The SDK carries the rule and the UserReason API applies it when a survey is about to be shown.

## Saved audiences

An audience you use for more than one survey can be saved once, under a name, and followed by each of them. In the dashboard it is **Audiences**; a survey picks one at the top of its Audience tab. In a definition it is a reference beside the survey's own audience:

```json
{
  "schema_version": 1,
  "saved_audience": { "audience_id": "66666666-6666-4666-8666-000000000001" },
  "audience": { "type": "field", "source": "context", "key": "country", "comparison": { "operator": "in", "value": ["NL", "DE"] } },
  "triggers": [{ "type": "page_load" }],
  "frequency": { "mode": "site_default" }
}
```

A person must match the saved audience **and** the survey's own audience. Use `{ "type": "always" }` for the survey's own when the saved audience says it all.

- **Editing saves a draft.** A draft reaches nobody: live surveys keep following the published version until you publish the audience.
- **Publishing reaches every survey that follows it, at once.** Those surveys are not republished. The dashboard lists the live surveys a publish will change before you confirm it, and every published version is kept.
- A survey cannot be published while it names an audience that is a draft or archived (`saved_audience_unpublished`), and an audience cannot be archived while a survey names it (`audience_in_use`).

A saved audience holds any rule on this page, previous surveys included. With the CLI, use `userreason audience` ([CLI](https://docs.userreason.com/cli/index.md#saved-audiences)).

## Why a person does or does not get a survey

In the dashboard, a user's page has an **Eligibility** section: every published survey, whether this person can get it right now, and each check behind that answer in the order delivery makes them: the survey is live, its trigger, each part of the audience, sampling, and the person's own limits (frequency, a screen-out, "Don't ask again"). A check after one that did not pass is not made. The answer comes from the same functions as real delivery.

There is no page and no event in that question, so triggers are listed instead of tested, and a condition on the current page or on an event's properties shows as "Not enough information". In the survey editor, **Check a person** under Audience does the same for a draft audience before you publish it.

## Sampling and priority

`sampling_percent` ranges from 0 to 100. Sampling is stable for the visitor, site, survey and `enrollment_epoch`; it does not reroll on every event. Use 100 while testing eligibility.

Keep `enrollment_epoch` unchanged on ordinary edits. Changing it starts a new enrollment: the sample is drawn again, and each person's counts and waits for this survey start again. In the dashboard that is **Start a new enrollment** on the Delivery tab, behind a confirmation. A person who chose "Don't ask again" is still not asked, and a previous-surveys rule with `all_enrollments` still sees earlier enrollments.

`priority` is a nonnegative integer. When multiple eligible surveys compete, higher priority wins; equal priorities are ordered by campaign ID. The SDK displays one widget at a time.

## Frequency

How often one person is asked is set in two places. The **site** has limits across all its surveys and a default for each survey. A **survey** follows that default or sets its own:

```json
{ "mode": "site_default" }
```

```json
{
  "mode": "custom",
  "max_shown": "3",
  "after_seen_seconds": 604800,
  "after_dismissed_seconds": 1209600,
  "after_completed": { "type": "never" },
  "after_screen_out": { "type": "never" }
}
```

| Field | Meaning |
| - | - |
| `max_shown` | The most times one person sees the survey in an enrollment; a positive decimal string, or `null` for no cap. |
| `after_seen_seconds` | How long to wait after they saw it and did nothing. `0` for no wait. |
| `after_dismissed_seconds` | How long to wait after they closed it. `0` for no wait. |
| `after_completed` | `{ "type": "never" }`, or `{ "type": "after", "seconds": 7776000 }` to ask again that long after they completed it. |
| `after_screen_out` | The same choice after a [screen-out](https://docs.userreason.com/prompts/definition/index.md). |

For a recurring survey, such as a quarterly satisfaction question, set `after_completed` to the interval and leave `max_shown` at `null` or high enough for the repeats.

### The site's policy

Site settings in the dashboard holds the policy; workspace owners and admins can change it. A change applies to live surveys at once.

| Setting | Meaning | Starting value |
| - | - | - |
| Gap between surveys | The least time between any two surveys for one person. | 1 day |
| Surveys per person | At most this many different surveys for one person in a window, such as 2 in 30 days. | No cap |
| Default for each survey | The five settings above, for surveys with `site_default`. | At most 3 times, 7 days after it was seen, 14 days after it was closed, never again after a completion or a screen-out |

The site's gap and cap apply to every survey, including those with their own settings.

### What counts

- **Seen, not let through.** A survey counts once it was confirmed on the person's screen. One that was let through and never appeared uses nothing up.
- **The latest outcome sets the wait.** Completed, screened out, closed, or seen and left alone.
- **"Don't ask again" is for good**, in every enrollment and whatever the settings.
- **The server decides, for the person.** The limits follow a recognized visitor across devices and an identified user across browsers. The SDK keeps no record of its own: the API tells it which surveys are held and it does not ask for those. Calling `reset()` or passing `force` to `show()` does not remove what the server knows.

A survey published before site policies existed has the earlier settings (`max_shown`, `max_responses`, `cooldown_seconds`, `global_cooldown_seconds`). It keeps behaving as it did until you publish it again; the editor shows the closest new settings to start from.

In the dashboard, a user's **Eligibility** says which limit holds a survey back and when it ends.
