Context
Segments are what turn stored events into something a marketer can act on, and the difference between a segment that updates in seconds and one that updates overnight is the difference between a campaign that lands during a flash sale and one that lands after it. ADR-012 makes a deliberate trade: the DSL is restricted so that incremental evaluation is possible. A fully general query language would be more expressive and would force a full recomputation on every event. Restricting the grammar to conditions whose truth can change only in known ways means a single event touches only the segments it can actually affect. Honesty about the limit is part of the design: a segment whose conditions are not incrementally evaluable is flaggednightly_only at creation, so the person building it knows before they
depend on it.
Scope (normative)
- The versioned JSON DSL: typed profile attributes, event aggregates over a window, membership in other segments and tiers, AND/OR/NOT.
- Incremental evaluation on each tracked event.
core.segment_memberswith entry and exit history.core.segment.enteredandcore.segment.exited.nightly_onlydetection at creation time, surfaced in the builder.- Full per-tenant recomputation as a safety net, at the data-freshness tier.
- Prebuilt RFM templates per vertical taxonomy.
Non-scope (normative)
- The segment builder UI —
frontend/console. - Sending to a segment —
messaging. - Predictive or ML-derived segments. Not planned: they are unexplainable to the person who has to defend the campaign.
- Extending the DSL ad hoc. A new node type is a versioned change with its own spec.
Behaviour (normative)
- The DSL is versioned. A segment records the DSL version it was written against, so a grammar change never silently reinterprets an existing definition.
- The grammar is closed: typed attribute comparisons, event aggregates (
count,sum(property),first_seen,last_seen) over last-N-days or all-time, membership in another segment or tier, and AND/OR/NOT. FORBIDDEN: arbitrary SQL, user code, unbounded joins. - Segment references are acyclic, enforced at creation. A cycle would recompute forever.
- Evaluation is incremental: a tracked event evaluates only the segments whose conditions reference that event name or the attributes it changed.
- Membership changes emit
enteredorexited. Re-evaluating with no change emits nothing — a segment that re-fires on every event would make campaign triggers unusable. core.segment_memberskeeps history withentered_atandexited_at. “Was this contact in the VIP segment when we sent that message” must be answerable.- A definition that cannot be evaluated incrementally is flagged
nightly_onlyat creation and the builder tells the user. Discovering it later, from stale data, is the failure this rule prevents. - Full recomputation runs per tenant at the freshness tier (nightly default, DEC-G5) as a safety net, and its divergence from incremental state is alerted, not silently corrected.
- Editing a definition creates a new version and triggers a full recomputation for that segment; membership under the old version stays in history.
Data (normative)
API (normative)
preview returns a count and a sample, never the full membership, and its budget is deliberately
looser: it is a design-time tool, not a runtime path.
Events (normative)
core.segment.entered and core.segment.exited, both available as outgoing webhooks and both
campaign triggers in messaging.
Acceptance criteria (normative)
- A purchase that crosses a segment threshold emits
enteredwithin 5 s p95. - Re-evaluating an unchanged membership emits nothing.
- Each DSL node type has a fixture proving its incremental diff is correct.
- A definition referencing a non-incrementally-evaluable condition is flagged
nightly_onlyat creation, and the response says so. - A cyclic segment reference is rejected at creation.
- Full recomputation reconciles a deliberately seeded drift and alerts rather than silently fixing it.
- Membership history answers “was contact X in segment Y on date Z” correctly.
- Negative: editing a definition does not rewrite historic membership rows.
Execution
Asynchronous pipeline. Evaluation inbackend/workers inside the ingestion processor pass; full
recomputation in backend/scheduler. The DSL evaluator lives in packages/core as a pure function
with exhaustive per-node fixtures.