Skip to content

Segments

A segment is a saved predicate list over your data: traits (from profiles in Postgres) and event history (from ClickHouse), combined with all or any. Membership is recomputed synchronously on every identify/track write, and on every backfill write, so an entry is visible to journeys immediately.

{
"match": "all",
"appId": ["app_storefront-dev", "app_storefront"],
"predicates": [
{ "kind": "trait", "name": "plan", "op": "eq", "value": "free" },
{ "kind": "event", "name": "signed_up", "op": "performed", "withinDays": 30 },
{ "kind": "trait", "name": "unsubscribed", "op": "notExists" }
]
}
  • match: all (default) or any. Deliberately flat: one list, no nested groups.
  • appId: optional; scopes the event side of the definition, as one app id or a list of them. Traits merge across apps, so the trait side is never app-scoped. Omit it to span apps. There is deliberately no filter on the source an event arrived through: a definition naming a pipe that is later archived would parse and evaluate empty forever.

Ops: eq, neq, gt, gte, lt, lte, contains, exists, notExists. The last two read no value; every other op requires one.

Comparison coerces form-friendly values before comparing: "150" matches 150 numerically, " TRUE " matches true for eq/neq (trimmed, case-insensitive), and contains is membership against array traits and substring against strings. Ordering never coerces booleans.

Ops: performed, notPerformed, with atLeast (default 1) and optional withinDays, a rolling window relative to evaluation time; absent means all history. Merged anonymous events count (see Identity merging).

A definition is shared, membership is not. Beside the definition, a segment carries an environments set saying where it works:

Terminal window
cow segments create --name "free trialers" --environments development \
--definition '{"match":"all","predicates":[…]}'

It defaults to both, and listing one is how you try a cohort in development before it touches production users. Membership is computed per environment, and only in the environments the segment declares, so a development-only segment never gains a production member.

An app id lives in exactly one environment, so the appId filter is an environment filter as well: name both of a product’s app ids and the definition matches it whichever environment it is evaluated in, name one and it matches there only. See Environments.

  • Create: membership is computed over existing history immediately, in every environment the segment declares, so a segment built after a backfill catches up and those entries fire.
  • Widen: adding an environment computes membership over that environment’s existing history the same way, so entries fire there for data that predates the change. Removing one drops its membership rows.
  • On write: every profile or event write recomputes the affected segments declared for that profile’s environment.
  • Transitions: entering or leaving emits system.segment_entered / system.segment_exited system events into ClickHouse. They are first-class journey triggers: { segment: "trialers" } in defineJourney rides the same path as a raw event.

POST /v1/segments/preview runs a not-yet-saved definition over existing profiles and returns the match count plus a small sample of member userIds:

Terminal window
cow segments preview --match all \
--predicates '[{"kind": "trait", "name": "plan", "op": "eq", "value": "free"}]'

The scan is bounded (1,000 profiles per call, rate-limited per org); when the cap is hit the count is reported as truncated, a floor rather than the exact number. The saved segment’s memberCount is always exact.

Time-decaying states are journeys, not predicates

Section titled “Time-decaying states are journeys, not predicates”

“Active in the last 30 days” evaluated by the wall clock is deliberately not an evaluator feature. Express it as a per-user journey with a durable timer that sets and unsets a trait, then point a segment at that trait. See Journey composition and the activity-decay template.