Targeting and frequency

Last updated
On this page

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 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() 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 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).

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.

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.