Docs Reference
Segments reference
Rule JSON, targets, operators, every field and the built-in templates.
A segment is a saved rule set over session_facts, the one-row-per-session table. Every segment compiles to a
single SQL statement, so "watched 50% of the VSL but did not opt in" is literally
max_video_pct >= 50 AND is_lead = false. The dashboard builder, the REST API (POST /segments,
POST /segments/preview) and the MCP tools (create_segment, preview_segment) all take the same JSON.
1. Rule JSON#
{
"target": "visitor",
"window_days": 30,
"site_id": "optional-site-uuid",
"funnel_id": "optional-funnel-uuid",
"all": [
{ "field": "max_video_pct", "op": ">=", "value": 50 },
{ "field": "is_lead", "op": "=", "value": false },
{ "any": [
{ "field": "clicked", "op": "contains", "value": "cta_pricing" },
{ "field": "max_scroll_pct", "op": ">=", "value": 75 }
]}
]
}
type SegmentTarget = 'session' | 'visitor' | 'person'
type Op = '=' | '!=' | '>' | '>=' | '<' | '<=' | 'contains' | 'not_contains' | 'in' | 'not_in' | 'exists' | 'not_exists' | 'starts_with'
type Condition = { field: string; op: Op; value?: unknown }
type Group = { all: Node[] } | { any: Node[] } | { not: Node }
type Node = Condition | Group
type SegmentRules = { target: SegmentTarget; window_days: number; site_id?: string; funnel_id?: string } & Group
all= AND,any= OR,not= negation. Groups nest without limit. An emptyallmatches everything, an emptyanymatches nothing.window_days(1..3650) limits sessions tostarted_at >= now() - window.site_idrestricts to one site;funnel_idto sessions that reached any step of that funnel.- Test sessions (
is_test = true) are always excluded; this is not configurable.
2. Targets and aggregation#
| target | Returns | How conditions are evaluated |
|---|---|---|
session |
session_id |
directly on each session row |
visitor |
visitor_id |
sessions grouped per visitor; conditions on aggregates (HAVING) |
person |
person_id |
same, grouped per identified person (person_id IS NOT NULL) |
Aggregation per field type when target is visitor or person:
| Type | Aggregate | Example |
|---|---|---|
| number | MAX |
max_video_pct >= 50 → the visitor's best session reached 50% |
| timestamp | MAX (latest) |
lead_at >= '2026-01-01' |
| boolean | BOOL_OR |
is_lead = false → never became a lead in any session |
| string | set semantics (any session) | utm_source = 'facebook' → at least one session came from facebook; != / not_* mean "no session matched" |
| string_array | union across sessions | clicked contains 'cta_pricing' → clicked it in any session |
| json | any session | video_by_id contains 'vsl_main' → has a key |
session_count is the number of sessions per visitor/person inside the window and is only valid for those targets.
3. Operators by type#
| Type | Operators | Value |
|---|---|---|
| number | = != > >= < <= in not_in exists not_exists | number; in takes an array of numbers |
| boolean | = != exists not_exists | boolean |
| string | = != contains not_contains starts_with in not_in exists not_exists | string; contains/starts_with are case-insensitive |
| string_array | contains not_contains in not_in exists not_exists | contains one string; in = overlaps with any of the array |
| timestamp | = != > >= < <= exists not_exists | ISO-8601 string or epoch ms |
| json | contains exists not_exists | contains = has top-level key |
exists / not_exists take no value. Empty strings count as not set.
4. Fields#
| Field | Type | Operators | Meaning |
|---|---|---|---|
session_id |
string | = != contains not_contains starts_with in not_in exists not_exists | Session id (uuid) |
site_id |
string | = != contains not_contains starts_with in not_in exists not_exists | Site id (uuid) |
visitor_id |
string | = != contains not_contains starts_with in not_in exists not_exists | Visitor id (uuid); null in anonymous mode |
person_id |
string | = != contains not_contains starts_with in not_in exists not_exists | Person id (uuid) once the visitor identified |
started_at |
timestamp | = != > >= < <= exists not_exists | Session start |
ended_at |
timestamp | = != > >= < <= exists not_exists | Session end (set when the session is closed) |
active_seconds |
number | = != > >= < <= in not_in exists not_exists | Seconds the tab was visible and the user active |
total_seconds |
number | = != > >= < <= in not_in exists not_exists | Wall-clock seconds between first and last event |
page_count |
number | = != > >= < <= in not_in exists not_exists | Pageviews in the session |
landing_url |
string | = != contains not_contains starts_with in not_in exists not_exists | Full URL of the first page |
landing_path |
string | = != contains not_contains starts_with in not_in exists not_exists | Path of the first page, e.g. /vsl |
exit_url |
string | = != contains not_contains starts_with in not_in exists not_exists | Last URL seen |
device_type |
string | = != contains not_contains starts_with in not_in exists not_exists | desktop |
browser |
string | = != contains not_contains starts_with in not_in exists not_exists | Browser family |
os |
string | = != contains not_contains starts_with in not_in exists not_exists | Operating system |
country |
string | = != contains not_contains starts_with in not_in exists not_exists | ISO country code from the edge |
region |
string | = != contains not_contains starts_with in not_in exists not_exists | Region/state from the edge |
consent_mode |
string | = != contains not_contains starts_with in not_in exists not_exists | auto |
consent_granted_at |
timestamp | = != > >= < <= exists not_exists | When consent was granted, if ever |
utm_source |
string | = != contains not_contains starts_with in not_in exists not_exists | utm_source of the session |
utm_medium |
string | = != contains not_contains starts_with in not_in exists not_exists | utm_medium |
utm_campaign |
string | = != contains not_contains starts_with in not_in exists not_exists | utm_campaign |
utm_content |
string | = != contains not_contains starts_with in not_in exists not_exists | utm_content |
utm_term |
string | = != contains not_contains starts_with in not_in exists not_exists | utm_term |
fbclid |
string | = != contains not_contains starts_with in not_in exists not_exists | Meta click id |
gclid |
string | = != contains not_contains starts_with in not_in exists not_exists | Google click id |
ttclid |
string | = != contains not_contains starts_with in not_in exists not_exists | TikTok click id |
referrer |
string | = != contains not_contains starts_with in not_in exists not_exists | Document referrer of the first page |
max_scroll_pct |
number | = != > >= < <= in not_in exists not_exists | Max scroll depth on the landing page, 0..100 |
scroll_by_path |
json | contains exists not_exists | Max scroll per path, e.g. {"/": 75} |
max_video_pct |
number | = != > >= < <= in not_in exists not_exists | Max video progress across all videos, 0..100 |
video_by_id |
json | contains exists not_exists | Per-video progress, e.g. {"vsl_main": {"max_pct": 52}} |
quiz_id |
string | = != contains not_contains starts_with in not_in exists not_exists | Quiz started in this session |
quiz_total_steps |
number | = != > >= < <= in not_in exists not_exists | Declared number of quiz steps |
quiz_last_step |
number | = != > >= < <= in not_in exists not_exists | Highest quiz step reached (1-based) |
quiz_completed |
boolean | = != exists not_exists | Quiz finished |
quiz_answers |
json | contains exists not_exists | Answers by step, e.g. {"1": "B"} |
clicked |
string_array | contains not_contains in not_in exists not_exists | data-track names clicked, e.g. {cta_hero,faq_3} |
forms_started |
string_array | contains not_contains in not_in exists not_exists | data-track-form names focused |
forms_submitted |
string_array | contains not_contains in not_in exists not_exists | data-track-form names submitted |
funnel_steps_reached |
json | contains exists not_exists | {" |
is_lead |
boolean | = != exists not_exists | Identified with email/phone (lead) |
lead_at |
timestamp | = != > >= < <= exists not_exists | When the lead was captured |
is_booked |
boolean | = != exists not_exists | A booking (Calendly or booked_call outcome) was attributed to this session |
booked_at |
timestamp | = != > >= < <= exists not_exists | When the booking happened |
is_customer |
boolean | = != exists not_exists | A purchase outcome was attributed to this session |
revenue_cents |
number | = != > >= < <= in not_in exists not_exists | Revenue attributed to this session, in cents |
custom |
json | contains exists not_exists | Everything sent via rf.set({...}); query with custom. |
session_count |
number (visitor/person only) | = != > >= < <= in not_in exists not_exists | Number of sessions of the visitor/person inside the window (visitor/person targets only) |
Custom fields#
Anything sent with rf.set({ plan_interest: 'premium', score: 42, vip: true }) lands in session_facts.custom and is
queryable as custom.<key> (keys: [a-z0-9_]{1,64}). The type is inferred from the value in the condition:
{ "field": "custom.plan_interest", "op": "=", "value": "premium" }
{ "field": "custom.score", "op": ">=", "value": 40 }
{ "field": "custom.vip", "op": "=", "value": true }
{ "field": "custom.plan_interest", "op": "exists" }
5. Built-in templates#
vsl_25_no_lead— VSL 25% – no lead (visitor, 30 days; sync: meta): Visitors in the last 30 days where max video % ≥ 25 and is lead = falsevsl_50_no_lead— VSL 50% – no lead (visitor, 30 days; sync: meta): Visitors in the last 30 days where max video % ≥ 50 and is lead = falsevsl_75_no_lead— VSL 75% – no lead (visitor, 30 days; sync: meta): Visitors in the last 30 days where max video % ≥ 75 and is lead = falsequiz_abandoners— Quiz abandoners (visitor, 30 days; sync: meta): Visitors in the last 30 days where quiz last step ≥ 1 and quiz completed = false and is lead = falsehigh_intent_no_lead— High intent – no lead (visitor, 30 days; sync: meta): Visitors in the last 30 days where max scroll % ≥ 75 and clicked is one of ['cta_pricing', 'cta_hero', 'cta_offer', 'cta_checkout', 'cta_buy', 'cta_book', 'cta_apply'] and is lead = falseleads_no_booking— Leads without booking (person, 30 days; sync: meta, google): People in the last 30 days where is lead = true and is booked = false and is customer = falsebookers_no_purchase— Bookers without purchase (person, 60 days; sync: meta, google): People in the last 60 days where is booked = true and is customer = falsereturning_no_lead— Returning visitors – no lead (visitor, 30 days; sync: meta): Visitors in the last 30 days where session count ≥ 3 and is lead = falsecheckout_abandoners— Checkout abandoners (visitor, 14 days; sync: meta, google): Visitors in the last 14 days where (clicked is one of ['cta_checkout', 'cta_buy', 'cta_add_to_cart', 'cta_order'] or scroll by path contains '/checkout') and is customer = falsewebinar_registered_no_show— Webinar registered – no show (person, 30 days; sync: meta, google): People in the last 30 days where is lead = true and is customer = false and not ((scroll by path contains '/live' or custom.webinar_attended is set))
Create them for a funnel with the MCP tool create_default_segments or from the dashboard.
6. What the compiler produces#
compileSegment(rules) returns { sql, params } with $1..$n placeholders; inlineParams(sql, params) renders a
self-contained statement for fs_refresh_segment. Example for the rules above:
SELECT sf.visitor_id
FROM session_facts sf
WHERE sf.is_test = false
AND sf.started_at >= now() - make_interval(days => $1)
AND sf.visitor_id IS NOT NULL
GROUP BY sf.visitor_id
HAVING (MAX(sf.max_video_pct) >= $2 AND BOOL_OR(COALESCE(sf.is_lead, false)) = $3
AND (COALESCE(BOOL_OR($4 = ANY(sf.clicked)), false) OR MAX(sf.max_scroll_pct) >= $5))
Only fields in the whitelist (SEGMENT_FIELDS) and custom.<key> are accepted; unknown fields or operators throw
before any SQL is built. Values are always parameters, never interpolated.
7. Tips#
- Build retargeting pools on
visitor(works for anonymous traffic through Meta CAPI); build suppression and Customer Match lists onperson. - Combine behaviour with the absence of an outcome:
is_lead = false,is_booked = false,is_customer = false. - Use
describeRules(rules)(or the dashboard preview) to read a rule set back in English before saving it. - Preview counts with
preview_segment/POST /segments/previewbefore enabling sync.
Last updated