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
}
}
]
}
| Leaf | Meaning |
|---|---|
sequence | 2–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_agg | count or sum (of the finite numeric attr_key) over the trailing over_days (1–365) window, compared with op (gte/lte) to value. |
relative | The 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
versionand apattern_fingerprint. Changing the pattern withPATCH /markers/{marker_id}increments the version, and later webhooks carry the new pair. status: pausedstops 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:previewevaluates 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
outcomedefaults fromtemplate(churn→absence,conversion→presence;customneeds it). Therecurrencetemplate is not available: express repetition as acountwindow_aggor asequencemin_count.- Every trailing window must fit inside the horizon: a
window_aggwithover_daysbeyondhorizon_days, or arelativewhose baselines reach before the anchor, is refused with400 invalid_argument. POST /v1/pattern-labels:previewdry-runs an unsaved rule over sampled entities, likePOST /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:
| Operation | Event-match labels | Pattern labels |
|---|---|---|
| create, list, get, update, restore | /v1/labels… | /v1/pattern-labels… |
| archive | DELETE /v1/labels/{label_id} | the same |
| stats, provenance | /v1/labels/{label_id}/stats, …/provenance | the 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.