Skip to main content

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 flagged nightly_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_members with entry and exit history.
  • core.segment.entered and core.segment.exited.
  • nightly_only detection 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)

  1. The DSL is versioned. A segment records the DSL version it was written against, so a grammar change never silently reinterprets an existing definition.
  2. 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.
  3. Segment references are acyclic, enforced at creation. A cycle would recompute forever.
  4. Evaluation is incremental: a tracked event evaluates only the segments whose conditions reference that event name or the attributes it changed.
  5. Membership changes emit entered or exited. Re-evaluating with no change emits nothing — a segment that re-fires on every event would make campaign triggers unusable.
  6. core.segment_members keeps history with entered_at and exited_at. “Was this contact in the VIP segment when we sent that message” must be answerable.
  7. A definition that cannot be evaluated incrementally is flagged nightly_only at creation and the builder tells the user. Discovering it later, from stale data, is the failure this rule prevents.
  8. 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.
  9. 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)

  1. A purchase that crosses a segment threshold emits entered within 5 s p95.
  2. Re-evaluating an unchanged membership emits nothing.
  3. Each DSL node type has a fixture proving its incremental diff is correct.
  4. A definition referencing a non-incrementally-evaluable condition is flagged nightly_only at creation, and the response says so.
  5. A cyclic segment reference is rejected at creation.
  6. Full recomputation reconciles a deliberately seeded drift and alerts rather than silently fixing it.
  7. Membership history answers “was contact X in segment Y on date Z” correctly.
  8. Negative: editing a definition does not rewrite historic membership rows.

Execution

Asynchronous pipeline. Evaluation in backend/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.

Open questions

Changelog

Delivery record

Not implemented yet.