Skip to content

Segment query language

Customer segments are defined in SegmentQL, a small query language you edit directly in the segment editor. If you have written a spreadsheet filter or a SQL WHERE clause, you already know most of it. This page is the complete language reference; for the list of every attribute you can filter on, see the segment attribute reference, and for the merchant-level guide see Customers & segments.

The editor scaffold

The editor always shows a four-line query:

FROM customers
SHOW customer_name, email, orders, amount_spent
WHERE amount_spent >= 500 AND last_order_date > -30d
ORDER BY amount_spent DESC

Only the WHERE clause is editable — it is the segment definition. FROM customers and SHOW are fixed (the SHOW columns mirror the preview table below the editor), and ORDER BY is rewritten by the Sort by control in the toolbar rather than typed. Keywords are case-insensitive when you type; the editor prints them uppercase.

As you type, the editor offers context-aware completions (attributes, then the operators valid for that attribute, then values), underlines invalid parts in red with a hover explanation, and shows a live count above the editor — Run (or Cmd/Ctrl + Enter) refreshes the matching-customers preview. Counts above 10,000 display as 10,000+. A segment cannot be saved while the query has errors.

Conditions and boolean logic

A condition is attribute operator value:

amount_spent > 1000
customer_tags CONTAINS 'vip'
last_order_date >= -90d

Combine conditions with AND and OR. AND binds tighter than OR, so

amount_spent > 1000 AND orders_placed IS NOT NULL OR customer_tags CONTAINS 'vip'

means (spent over 1,000 AND has ordered) OR is tagged vip. Use parentheses to group explicitly — they always win over precedence:

amount_spent > 1000 AND (customer_location.country = 'IN' OR customer_tags CONTAINS 'vip')

There is no NOT (...) group operator. Negation is written per condition with the negative operator forms: !=, NOT CONTAINS, NOT IN, NOT_MATCHES, IS NOT NULL, NOT IN LAST.

Operators

Which operators an attribute accepts depends on its type — the attribute reference lists them per attribute, and the editor only ever suggests valid ones.

OperatorMeaningExample
= / !=equals / not equalscustomer.state = 'ENABLED'
> >= < <=numeric or date comparisonamount_spent >= 500
BETWEEN x AND yinclusive rangeamount_spent BETWEEN 100 AND 999.99
CONTAINS / NOT CONTAINSlist membership, or substring on textcustomer_tags CONTAINS 'wholesale'
CONTAINS ALL (...) / CONTAINS ANY (...)every / at least one of the listed valuescustomer_tags CONTAINS ALL ('vip', 'newsletter')
STARTS WITH / ENDS WITHtext prefix / suffixcustomer.email ENDS WITH '@gmail.com'
IN (...) / NOT IN (...)one of a list of valuescustomer_location.country IN ('IN', 'AE')
IS NULL / IS NOT NULLvalue absent / presentcustomer.birthday IS NOT NULL
IN LAST n UNIT / NOT IN LAST n UNITwithin the last n days/weeks/months/yearslast_order_date IN LAST 90 DAYS
IN NEXT n UNITwithin the coming n days/weeks/months/yearsloyalty MATCHES (next_expiration_date IN NEXT 30 DAYS)
IN MONTHrecurring date falls in a monthcustomer.birthday IN MONTH May
ON month dayrecurring date falls on a daycustomer.birthday ON May 24
MATCHES (...) / NOT_MATCHES (...)function attributes — see belowproducts_purchased MATCHES (id = '…', quantity >= 2)

On function attributes, IS NOT NULL means has ever and IS NULL means never — for example orders_placed IS NULL is “has never placed an order”.

Values

Text

Single-quoted. Escape a quote as \' (typing '' also works; the editor prints \').

customer.note CONTAINS 'says \'call after 6\''

Text comparison is case-sensitive, with three deliberate exceptions: tag values, discount codes, and color metafields match case-insensitively. CONTAINS on text attributes is a case-insensitive substring match; CONTAINS on list attributes (like customer_tags) is an exact-member match.

Numbers and money

Plain numbers with a . decimal separator and no thousands separators. Money values are written in major currency unitsamount_spent > 500 means ₹500 (or your store currency), not 500 paise.

Booleans

true / false, unquoted:

product_review MATCHES (has_video = true)

Dates and times

FormExampleNotes
Absolute date2026-05-25yyyy-mm-dd
Absolute date-time2026-05-25T18:30:00interpreted in the store timezone
Offset-30d, -4w, -6m, -1y, +7dunits: d w m y, relative to now
Namedtoday, yesterday, tomorrowresolved daily in the store timezone

Offsets and named dates are dynamic: a segment using last_order_date > -30d re-evaluates every day, so membership stays current without edits.

Measurements

Dimension, volume, and weight metafields take a number followed by a unit; values are compared with unit conversion (a 5 cm condition matches a value stored as 50 mm):

metafields.specs.max_width <= 5 cm
metafields.specs.capacity BETWEEN 250 AND 750 ml

Resource IDs and named values

Attributes that reference products, variants, collections, locations, brands, or email campaigns take the resource’s ID — and you never have to type one: the editor’s autocomplete searches by name and inserts the ID for you, and hovering an ID shows the resource title. The same name-based suggestions cover tag values (customer_tags, products_purchased.tag) and taxonomy categories (products_purchased.category), which insert the quoted value instead of an ID.

Function attributes and MATCHES

Function attributes describe related records — orders, purchased products, reviews, gift cards, event tickets, and so on. Their conditions go inside MATCHES ( ... ), separated by commas (a comma means AND; OR is not allowed inside the parentheses):

orders_placed MATCHES (count >= 3, date >= -6m)
products_purchased MATCHES (tag = 'sportswear', sum_quantity >= 3)
product_review MATCHES (product_id = '018f2e6a-…', has_video = true)
gift_cards MATCHES (balance > 0, next_expiry_date IN NEXT 14 DAYS)

All conditions inside one MATCHES scope apply to the same related record (or the same aggregation over them) — “rated this product below 3 stars” is one scope:

product_review MATCHES (product_id = '018f2e6a-…', rating <= 2)

NOT_MATCHES ( ... ) (also accepted as NOT MATCHES) selects customers with no related record satisfying the scope. IN ( ... ) value lists are allowed on ID parameters inside a scope:

products_purchased MATCHES (id IN ('018f…', '018e…'), date >= -90d)

Two functions have special parameter forms:

  • Distancecustomer_within_distance MATCHES (coordinates = (13.0827, 80.2707), distance_km = 25) (use distance_mi for miles).
  • Anniversary sugaranniversary('customer.birthday') BETWEEN today AND +30d is accepted as shorthand for the recurring-date operators on any recurring-date attribute; the editor rewrites it to the canonical form when it reprints the query.

Metafield paths

Customer metafields are addressed as metafields.<namespace>.<key> (customer.<namespace>.<key> is also accepted):

metafields.b2b.price_band = 'gold'
metafields.facts.renewal_date IN NEXT 30 DAYS

Inside a MATCHES scope, metafields of the related record use metafield.<namespace>.<key> and resolve against that record’s owner type:

products_purchased MATCHES (metafield.materials.fabric = 'cotton')

A metafield must have Use as filter enabled on its definition to appear in segments. Namespaces or keys that don’t fit the identifier syntax are written in backticks. See the attribute reference for which value types are filterable and how list metafields behave.

Grammar summary

where = orExpr
orExpr = andExpr { OR andExpr }
andExpr = primary { AND primary }
primary = "(" orExpr ")" | condition
condition = attribute operator [ value ]
| function ( "MATCHES" | "NOT_MATCHES" ) "(" param { "," param } ")"
| function ( "IS NULL" | "IS NOT NULL" )
param = subfield operator [ value ]
value = string | number [ unit ] | boolean | date | offset | id
| "(" value { "," value } ")"

Worked examples

-- Lapsed high-value customers
amount_spent >= 5000 AND last_order_date NOT IN LAST 90 DAYS
-- Subscribed locals near the flagship store
customer.email_subscription_status = 'SUBSCRIBED'
AND customer_within_distance MATCHES (coordinates = (13.0827, 80.2707), distance_km = 25)
-- COD-heavy repeat buyers (for prepaid-incentive campaigns)
orders_placed MATCHES (count >= 3) AND orders_placed MATCHES (is_cod = true, date >= -6m)
-- Bought from the Beauty category but never left a review
products_purchased MATCHES (category = 'ha-15') AND product_review IS NULL
-- Loyalty members with points expiring this month
loyalty MATCHES (status = 'enrolled', next_expiration_date IN NEXT 30 DAYS)
-- Waiting for a restock of a specific product
stock_notifications MATCHES (product_id = '018f2e6a-…', status = 'active')
-- Birthday campaign audience
customer.birthday IN MONTH May AND customer.email_subscription_status = 'SUBSCRIBED'

How segments stay current

Dynamic segments are recomputed automatically: profile edits, orders, consent changes, loyalty/review/referral activity, and metafield writes all mark the affected customers for re-evaluation within seconds, and date-based rules (offsets, IN LAST, birthdays) are re-scanned daily in the store’s timezone. You never need to press anything to keep a segment fresh — Refresh on the segment page just forces an immediate pass.