Skip to main content

Patterns and markers

A pattern is a small, deterministic rule over an entity's own events: "a losing bet followed by a deposit within 2 hours", "outbound transfers summing to at least 1,000 over 30 days", "three times the usual weekly sessions". Markers are named patterns that every scorer of a slot evaluates at scoring time. A marker's state rides on the entity's state row, and a marker that turns active sends one marker.fired webhook.

Markers are outputs only. They never become model inputs: creating, pausing or deleting a marker changes no dataset shard, fingerprint, embedding or score.

The pattern language​

A pattern is a JSON document with three leaf kinds, combined with all, any and not (at most three nested combinators and eight leaves):

{
"all": [
{
"sequence": {
"steps": [
{"match": {"event_type_pattern": "bet.settled",
"attribute_filters": [{"attr_key": "outcome", "op": "eq", "value": "loss"}]}},
{"match": {"event_type_pattern": "deposit.completed"}}
],
"within_seconds": 7200
}
},
{
"window_agg": {
"agg": "count",
"match": {"event_type_pattern": "deposit.completed"},
"over_days": 1,
"op": "gte",
"value": 1
}
}
]
}
LeafMeaning
sequence2–4 ordered steps; each consecutive pair at most within_seconds apart (1 s to 90 days) with strictly increasing timestamps. Matches never reuse events and never overlap; min_count (default 1) is how many are needed.
window_aggcount or sum (of the finite numeric attr_key) over the trailing over_days (1–365) window, compared with op (gte/lte) to value.
relativeThe same aggregate compared with factor × the mean of the preceding baseline_windows (1–8) windows of the same length. min_current (default 1 for count, required for sum) is a floor on the current window, so a quiet baseline can't turn any activity into an "infinite" rise.

Every leaf shares the match object you already know from derived labels: an anchored event-type glob (* matches any run of [a-z0-9_.]) plus up to 16 attribute filters. Trailing windows end at the evaluation time and exclude their start instant: a 30-day window evaluated at T covers (T − 30 days, T]. Missing, text and boolean values never count toward a sum.

Patterns that break a rule — an unknown field or operator, a fourth nested combinator, a ninth leaf, a five-step sequence, a sum without attr_key — are rejected with 400 invalid_argument, and the error names the rule.

Markers​

Create a marker and enable it on a deployment slot:

curl -X POST "$TESSERA_API_BASE/markers" \
-H "authorization: Bearer $TESSERA_API_KEY" -H "content-type: application/json" \
-d '{"name": "loss_chase", "pattern": { ... }, "slots": [{"slot_id": "slot_...", "enabled": true}]}'
  • Each marker has a version and a pattern_fingerprint. Changing the pattern with PATCH /markers/{marker_id} increments the version, and later webhooks carry the new pair.
  • status: paused stops evaluation entirely.
  • A slot evaluates at most 32 active markers. Enabling a 33rd fails with 400 failed_precondition (marker_slot_cap_exceeded).
  • Marker names are registered with your leakage policy, like label names.
  • POST /markers:preview evaluates an unsaved pattern over up to 200 sampled entities and returns how many would be active. Nothing is saved.

When markers are evaluated​

Each time a scorer writes an entity's state, in batch refreshes and in real-time scoring, it evaluates the slot's enabled markers. The scorer reads the entity's recent raw events, including event types and attributes the model never sees, up to the same W rows it already reads. The evaluation time is the refresh's as_of cutoff in batch, and the newest event in real time.

If a window reaches past the oldest event the scorer read, the marker state says "truncated": true. The verdict is still reported, but you can see it is based on partial history.

Reading marker state​

GET /slots/{slot}/entities/{type}/{id}/markers returns the complete state of every evaluated marker:

{
"entity": {"type": "player", "id": "p-1042"},
"slot": "prod",
"markers": [
{
"marker_id": "mkr_01hv4d0s9t2v5x8z1c4f7j0m3q",
"version": 1,
"pattern_fingerprint": "5f0e…",
"active": true,
"since": "2026-01-01T00:30:00Z",
"window": {"start": "2025-12-31T00:30:00Z", "end": "2026-01-01T00:30:00Z", "truncated": false}
}
]
}

The state read (GET …/state) also lists the active markers in signals.markers when the bundle has signals enabled, with fired_at as the start of the current episode.

The markers read honors suppression sets exactly like the state read: an entity in a suppression set that covers reads returns 404 as if it had no state, and if suppression state can't be checked the request fails with 503 rather than returning unfiltered markers.

The marker.fired webhook​

A marker sends exactly one marker.fired per episode: when it goes from inactive to active for a (entity, marker, version). While it stays active, nothing more is sent. When it clears and later becomes active again, a new episode starts. A new marker version also starts a new episode. Delivery is at-least-once, so deduplicate on the envelope id. The payload names the marker, version and fingerprint, the slot and entity, the evaluated window and up to 32 of the matching events.

Pattern labels​

A pattern can also be a label: a binary training target whose outcome is the pattern evaluated over each anchor's future window (t, t + horizon_days]. Anchors, open-horizon censoring and emission work exactly as for other derived labels; maturation_days always equals horizon_days.

{
"name": "outbound_1000_30d",
"entity_type": "account",
"source": {
"kind": "derived",
"rule": {
"template": "custom",
"horizon_days": 30,
"outcome": "presence",
"predicate": {
"kind": "pattern",
"pattern": {
"window_agg": {
"match": { "event_type_pattern": "transfer.outbound" },
"agg": "sum",
"attr_key": "amount",
"over_days": 30,
"op": "gte",
"value": 1000
}
}
}
}
}
}
curl -X POST "$TESSERA_API/v1/pattern-labels" \
-H "Authorization: Bearer $TESSERA_KEY" -H "Content-Type: application/json" \
--data @pattern-label.json
  • outcome defaults from template (churn → absence, conversion → presence; custom needs it). The recurrence template is not available: express repetition as a count window_agg or a sequence min_count.
  • Every trailing window must fit inside the horizon: a window_agg with over_days beyond horizon_days, or a relative whose baselines reach before the anchor, is refused with 400 invalid_argument.
  • POST /v1/pattern-labels:preview dry-runs an unsaved rule over sampled entities, like POST /v1/labels:preview.
  • PATCH /v1/pattern-labels/{label_id} edits the description and replaces the rule until the label's first materialization; afterwards the rule is frozen (derived_rule_frozen) — create a new label instead.

Pattern labels and /labels​

The /labels resource keeps its released shape, which cannot represent a pattern rule, so the two resources split as follows:

OperationEvent-match labelsPattern labels
create, list, get, update, restore/v1/labels…/v1/pattern-labels…
archiveDELETE /v1/labels/{label_id}the same
stats, provenance/v1/labels/{label_id}/stats, …/provenancethe same

GET /v1/labels never lists pattern labels, and GET, PATCH and :restore on /v1/labels/{label_id} answer 400 failed_precondition (reason pattern_label) for one. Label names are unique across both. In a dataset eligibility preview a pattern label reports its derived counts without rule; read the rule with GET /v1/pattern-labels/{label_id}. Domain packs whose label recipes carry a pattern rule install them as pattern labels.