Skip to main content

Bands, entity alerts and dispositions

Tessera turns per-entity scores into alert episodes you can triage, and measures how often those alerts were right. Everything is configured per deployment slot as data and served from the alerting API (alerts tag); measured precision is served under monitors.

Alert rules​

A rule is a predicate over the scored entity state — the same predicate language the platform uses everywhere (predicate.json): a tree of all / any / not nodes over leaves that select a score, a propensity, behavior surprise or the segment. Alert rules can also select:

  • {"kind": "band", "label": …} — the band of a bound label (see Bands), e.g. "op": "eq", "value": "critical"; a label without a binding never matches;
  • {"kind": "quiet_state", "field": "silence_seconds" | "silence_threshold_crossed"} — the quiet-state fields of a slot with observation time enabled, in seconds; a write without them never matches.

These two selects exist only for alert rules: audiences and suppression sets reject them with invalid_argument.

{
"name": "lateral movement",
"predicate": {
"all": [
{ "select": { "kind": "score", "label": "compromise_7d" }, "op": "gte", "value": 0.9 },
{ "select": { "kind": "behavior_surprise" }, "op": "gte", "value": 0.95 }
]
},
"cooldown_seconds": 3600,
"auto_resolve_after_seconds": 86400
}
  • POST /slots/{slot}/alert-rules creates a rule (admin scope). Unknown fields, operators or selects are rejected (invalid_argument); the predicate is validated by the platform's single predicate validator.
  • A score or band select must name a label the slot's active bundle scores: a band needs a binary label, and a multiclass label needs one of its class values. A rule that could never match is rejected with invalid_argument naming the label and the bundle, on create and when a PATCH changes the predicate.
  • A slot holds at most 64 active rules (configuration data). Creating or re-activating a rule beyond the cap fails with failed_precondition (reason alert_rule_limit_reached); paused rules do not count.
  • PATCH updates any field (label_bridge: null removes the bridge); pausing a rule stops matching within 30 seconds. DELETE retires the rule while keeping its episodes, dispositions and audit history.

The packaged quiet-state rule​

POST /slots/{slot}/alert-rules:install-went-quiet (admin scope) installs the packaged entity.went_quiet rule on a slot. It is an ordinary rule — same episodes, cooldown, sweep, dispositions and measured precision — whose episodes publish entity.went_quiet instead of alert.raised:

{ "threshold_seconds": 1209600, "cooldown_seconds": 604800 }

All fields are optional. Without threshold_seconds the rule matches any configured silence-threshold crossing; with it, crossings of at least that many seconds. It needs observation time with silence thresholds on the slot: without quiet-state fields the rule simply never matches. A slot holds at most one packaged rule (a second install answers 409 already_exists; PATCH or delete the existing one), and it takes an active-rule seat like any rule.

Episodes​

Rules are evaluated on every state write — event-driven and scheduled re-scores alike — and a match opens an episode:

  • at most one open episode per rule and entity; further matches only advance last_matched_at (recorded at one-second granularity);
  • the episode keeps an immutable opening snapshot: scores, bands (for bound labels), the behavioral evidence that explains the score, and the score's provenance (freshness);
  • after an episode closes, a new one opens only once cooldown_seconds have elapsed since the previous episode opened;
  • an episode unmatched for auto_resolve_after_seconds is auto-resolved (the resolution sweep runs every 30 seconds).

GET /slots/{slot}/alert-episodes?status=open|resolved&rule_id= lists episodes newest first; GET …/alert-episodes/{episode_id} returns one.

Deleting an entity's data (a data-subject deletion) also removes its alert episodes, their dispositions and its alert deliveries.

Webhooks​

Each transition publishes exactly one webhook through your webhook endpoints (signed with Tessera-Signature — verify it exactly as for every other event type):

EventWhen
alert.raisedan episode opens
alert.resolvedan episode closes (auto_resolved or dispositioned)
score.band_changeda bound label's band changes between two writes
entity.went_quietthe packaged quiet-state rule opens an episode

Every alert payload carries the opening snapshot, so a receiver never needs to call back for context — up to the 64 KiB envelope bound: payloads over 64 KiB omit signals, then evidence/frame; fetch the episode via REST (GET …/alert-episodes/{episode_id}) for the full snapshot. Envelopes are delivered at least once: deduplicate on the envelope id (a re-delivery of the same transition has the same id). Alert webhooks pass the platform's delivery suppression filter immediately before they are sent: a suppressed entity's alert never leaves the platform, while its episode stays available for triage. A transition whose delivery could not be decided or sent is retried by the sweep (with a growing delay, up to 15 minutes) with the same envelope id, and never delays alerts behind it.

Envelopes are not ordered relative to each other, and a transition can arrive late: when a batch refresh's alert evaluation fails, it is evaluated again later exactly as it would have been then (the refresh's instant, the refresh's rules, the scores as written), so a score.band_changed can arrive after a later band change of the same entity. To apply band changes in order, order them per entity by data.freshness.event_cursor (<microseconds>#<event id>: compare the number before # first), then by data.freshness.computed_at; a change older than the one you applied last is history, not the current band. Such a late evaluation applies the rules as they were when the refresh ran, except that a rule you paused or deleted since then, and a label you unbound, is skipped: neither raises a late alert.raised or score.band_changed. If a refresh's evaluation could not be recorded at all, the next refresh evaluates what that refresh wrote exactly as that refresh would have (its rules and bindings, its instant); for an entity whose score a later write already replaced, only the band change is recorded (the newer state wins). A refresh's rules are kept for 35 days after anything last needed them (every later refresh that still has its writes to evaluate needs them again). Only when no refresh needed them for 35 days, because for that long the slot had no rules and no band bindings, no refresh ran, its active bundle was not live (shadow), or every refresh failed at its start, are your current rules used instead (their current versions, and without a band view for an entity whose band did not change); that differs from the refresh's own rules only if you changed the slot's rules or bindings meanwhile.

Dispositions​

Record what an alert turned out to be:

{ "disposition": "true_positive", "reason": "confirmed by incident response" }

POST /slots/{slot}/alert-episodes/{episode_id}:disposition accepts true_positive, false_positive, benign or duplicate. Each judgment is appended (the latest wins), written together with its audit record in one transaction, and closes an open episode. Send an Idempotency-Key to make retries safe.

Feeding labels back. A rule may carry a label_bridge naming a binary, event-sourced label of the slot's entity type. Then every true_positive / false_positive disposition becomes one label event (value true / false) observed at the moment of the disposition — never backdated to when the alert opened — and is usable by the next dataset build. benign and duplicate emit nothing. The label events arrive through the platform-managed alerts:dispositions stream source; if another source of that name exists in the environment, or the platform source was disabled, bridged dispositions fail with failed_precondition (reason label_bridge_source_conflict or label_bridge_source_inactive) until it is renamed or re-enabled — or the rule's label_bridge is removed. Label events are published right after the judgment commits and retried automatically (with a growing delay) if the platform cannot publish one at that moment; a bridge the label router refuses for an event is skipped and logged — the judgment itself always lands.

Measured precision​

GET /slots/{slot}/monitors/alert-precision?rule_id=&from=&to= returns, per rule, the episodes opened in the window, how many were dispositioned, the true/false-positive counts and

precision = true_positive / (true_positive + false_positive)

Undispositioned, benign and duplicate episodes count in volume but never in the denominator; precision is null until something was judged. The unfiltered listing covers the slot's live rules; a deleted rule's precision stays readable with rule_id=.

GET /slots/{slot}/monitors/alert-precision/weekly?rule_id=&from=&to= serves the realized alert-precision series: one point per rule and ISO week (Monday, UTC) over the episodes opened that week, with the same counts and precision, newest week first. Points are recomputed every hour over the trailing 12 weeks, so a disposition shows up in its week's point within about an hour — including a late judgment of an older episode.

Bands​

Bands bind, per label, a fingerprinted banding policy — the same policy object your evaluation measured:

{
"bindings": [
{
"label": "compromise_7d",
"policy": {
"bands": [
{ "band": "critical", "min_score": 0.9 },
{ "band": "elevated", "min_score": 0.5 }
],
"list_size": 100,
"alert_budget": 25
},
"policy_fingerprint": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
]
}

The policy_fingerprint in this example is a placeholder shaped like a real one (64 lowercase hex characters) — use the fingerprint of your own policy, the one recorded with its evaluation.

PUT /slots/{slot}/bands replaces the slot's bindings (an empty list clears them); each policy_fingerprint must equal the fingerprint the platform computes for its policy, or the binding is rejected. Bands are derived from the bound policy whenever they are emitted — never stored with the entity state — and slots without bindings carry no band fields at all. Episode snapshots, alert payloads and score.band_changed carry bands; the existing state reads, exports and score.updated payloads are unchanged. Read bands for those surfaces through two additive operations that apply the same bound policy:

  • GET /slots/{slot}/entities/{entity_type}/{entity_id}/bands returns the bands of the entity's served state (read scope; bands is {} when the slot has no binding);
  • POST /slots/{slot}/bands:derive derives bands for score sets you already hold — the score__<label> columns of an export row, or the scores of a score.updated payload:
{ "items": [{ "key": "shp-00042", "scores": { "compromise_7d": 0.93 } }] }

Availability​

Rule evaluation and band derivation use the platform's shared predicate evaluator and banding implementation. On a deployment where either is unavailable, the affected writes answer failed_precondition with reason predicate_evaluator_unavailable or banding_unavailable — nothing is stored unvalidated, and reads, episodes, dispositions and measured precision keep working.