Incidents

How incidents open, group around a root cause, and close, and how operations can be undone.

An incident is a durable record that something failed: a rule's action, a compilation, a data contract, or a promise about freshness. It carries a severity, the nodes it affects, a history of what happened to it, and who is working on it. Nistor opens and closes most incidents itself. A person accepts, acknowledges, assigns, snoozes or resolves them.

This page covers how incidents open, how related failures are grouped, how Nistor suggests what caused one, and how a destructive operation can be undone. It builds on Rules and scheduling, because most incidents start with a rule's run, and on Lineage and impact, because grouping and blast radius follow the graph.

What an incident holds#

Field Meaning
incident_type The kind (see below).
severity critical, high, medium or low, set when the incident opens.
status open, acknowledged, resolved or closed.
environment, affected_nodes Where it happened and which nodes it touches. The environment is the highest-priority one among the nodes.
failure_count, last_failure_datetime How many times it has failed since it opened, and when it last did.
assignee, reporters, watchers, assigned_team Who works on it, who validates the fix, who is notified, and the owning team (from node labels or a project-to-team assignment).
incident_class, response_tier How to treat it (see Class and tier).

Nistor advises the severity once, when the incident opens. It weighs the environment (production weighs most), the kind of failure (a permission gap weighs less than a failed run), how critical the node is (its criticality: or tier: label), how many nodes sit downstream, and, a little, how often the run was retried. An incident raised inside a maintenance window is marked as not counting towards time to acknowledge and time to resolve.

How incidents open#

Kind Raised when
action_run A rule's action fails. This is the common one.
data_contract An asset fails one dimension of an active data contract (see Data Trust).
delivery_slo An asset arrives later than promised: a rule's freshness_expectation or if.refresh_sla is missed, or a monitored asset burns its freshness SLO or misses its delivery deadline.
loss_of_signal Something stopped reporting: a monitored asset past its heartbeat, a rule whose triggers went silent, a rule whose target nodes no longer exist, or the scheduler itself firing nothing.
predicted_freshness A forecast says an asset will miss its freshness deadline.
rule_permission The identity Nistor runs as lacks a right that a rule's action needs on one of its targets.
extraction_failure, extraction_partial_failure A compilation could not read a source at all, or could not read part of it.
consolidation_failure A background consolidation job failed for one domain.
plugin_read_only A plugin is not active, so its write routes refuse writes and its background work is held.

Enterprise modules declare kinds of their own: data_quality from the Data quality module, budget_exceeded and cost_anomaly from FinOps, and ai_feedback_spike from the AI module. GET /api/v1/incidents/kinds lists the kinds registered in your deployment.

Many detectors are off by default, because a detector that is wrong for your estate can open thousands of incidents at once. Where a detector has a dry-run endpoint, run it before you turn the detector on: it reports what the detector would open and writes nothing. Those endpoints accept only service-account callers. The settings are environment variables of the backend.

Setting Default What it does
DELIVERY_SLO_INCIDENTS_ENABLED off Turn a sustained SLO burn, or a missed delivery deadline, of a monitored asset into one delivery_slo incident. DELIVERY_SLO_SUSTAIN_CYCLES (3) sets how many consecutive 30-minute health cycles count as sustained.
HEARTBEAT_INCIDENTS_ENABLED off Open loss_of_signal when a monitored asset has had no successful run within HEARTBEAT_EXPIRATION_MULTIPLE (2.0) times its expected interval. Dry run: POST /api/v1/incidents/check-heartbeat?dry_run=true.
RULE_TRIGGER_LIVENESS_ENABLED off Open loss_of_signal when a rule's trigger nodes stop writing. Dry run: POST /api/v1/incidents/check-trigger-liveness, which is a dry run unless you pass dry_run=false.
RULE_TARGET_RESOLUTION_ENABLED off Open loss_of_signal for an active rule whose explicit node ids have all left the graph. Dry run: POST /api/v1/incidents/check-rule-targets, likewise.
RULE_REFRESH_SLA_ENABLED off Evaluate if.refresh_sla blocks. Dry run: POST /api/v1/incidents/check-refresh-sla, likewise.
RULE_PERMISSION_INCIDENTS_ENABLED off Open rule_permission after a right has been missing for RULE_PERMISSION_DENIED_INCIDENT_AFTER_SECONDS (86400). The check itself, RULE_PERMISSION_PREFLIGHT_ENABLED, is on.
FRESHNESS_FORECAST_CREATE_INCIDENTS off Open predicted_freshness for a forecast breach. The forecast itself, FRESHNESS_FORECAST_ENABLED, is on and sends predicted_freshness_breach alerts to the policies that match them.
INCIDENT_SUPPRESSION_GATE_ENABLED off Never create an incident that an active permanent suppression rule matches, and count the failure on the rule instead (see Working an incident).
STALE_TIER_ESCALATION_V1_ENABLED off A daily digest per team of incidents past a severity-specific age.
SCHEDULER_LIVENESS_ENABLED on One loss_of_signal incident per environment when the scheduler fires nothing: due on-hold actions are overdue, or active cron rules were due with no run.
INCIDENT_GENERIC_LIFECYCLE_ENABLED on The shared open-and-close logic for runs (see below). Set it identically on the backend and the scheduler.
MAINTENANCE_SUPPRESSES_INCIDENTS on Record an incident that would open inside an alert-suppression maintenance window as an expected one instead of paging.

A rule's freshness_expectation has no flag of its own: the freshness evaluator opens its delivery_slo incident whenever the deadline passes without a fresh target.

One incident per failing thing#

With the generic lifecycle on (the default), an action_run incident is identified by the action type and the node. The first failed run opens it. Each further failure of the same pair adds to failure_count instead of opening a second incident. A later success closes it, whichever part of Nistor saw that success: a refresh that the scheduler ran and a refresh that a poller saw happen outside Nistor close the same incident.

Some failures never open one. A run of a rule in tuning mode (shadow_mode), and a test run, open none. When one Dataform invocation fails, only the actions that really failed carry the incident. An action that Dataform cancelled because a sibling failed, or because the invocation timed out, is collateral and does not.

Class and tier#

Nistor sorts each incident once, when it opens, and stores the result:

incident_class Meaning response_tier
platform A real failure of a run or of a source, or a fault of the platform itself. page for an action_run failure, ticket for every other kind.
configuration A gap a person must fix: a missing permission, an authentication problem, an API that is not enabled, or broken pipeline code. ticket. The cost-estimation pre-flight gap, which never blocks a run, is log.
data_quality The data is wrong, late or in breach of a contract: the data_quality, data_contract and delivery_slo kinds. ticket
expected Already marked expected, or a test run. log

page means someone should be woken, ticket means it belongs in the backlog, and log means record it only. The tier is advice. Your alert policies decide who is notified.

Grouping around a root cause#

Three mechanisms keep one cause from showing up as fifty incidents.

Read-time groups. The console collapses incidents that share a dedup_fingerprint into one group with a member count. A critical incident is never merged into a group.

Root-cause clusters. Nistor normalises the failure text of an incident and hashes it into an error_signature. The normalisation replaces identifiers, timestamps, dates and numbers with placeholders and, when a failure message joins one reason per table, keeps only the first reason. With the source_project, the signature groups the open and acknowledged incidents that share a cause. GET /api/v1/incidents/{id}/root-cause returns the cluster size, the oldest incident in it and the others. The incident pane shows a "Root Cause cluster" card when the cluster has more than one member.

Folding under an upstream incident. When an ancestor within four hops of a node already has an open incident, Nistor folds the node's new incident under it: the same failure, seen one step later. The child is recorded with a root_incident_id, and the console shows it under its root. When the root closes, the link is cleared and the child shows on its own again. Lineage is not causality, so some incidents never fold: a configuration incident or an extraction failure is never caused upstream, and a data incident (data_quality, data_contract or delivery_slo) only explains other data incidents.

What changed?#

An incident pane can show the changes that happened before it, ranked by how likely each is to be the cause. The console calls the panel "What changed?" and the API calls the feature attribution. Nistor ranks candidates. It does not state a cause.

The feature is off by default. Set DATA_GIT_BLAME_ENABLED to true; until you do, every route answers 404. Reading it needs the attribution:view permission, which the viewer role template includes.

For an incident, Nistor takes the failing nodes and their ancestors (up to ATTRIBUTION_MAX_ANCESTOR_DEPTH, 6 hops) and collects the changes in the window before the incident (ATTRIBUTION_WINDOW_DAYS, 14 days) from four sources: node definition changes, deployments (the Git commit a node was deployed from), rule changes, and GitOps syncs. It scores each change:

Component Weight Reads as
Time proximity 0.40 Halves for every day before the incident. A change after the incident scores zero overall.
Lineage distance 0.25 Falls by 40% at each hop upstream.
Break class 0.25 A prior for the kind of change and the shape of its diff.
Column overlap 0.10 A boost when the changed columns include columns the failing node used.

A score of 0.65 or more is high confidence, 0.40 or more is medium, and the rest is low. The routes, under /api/v1/attribution, are GET /incidents/{id} (the ranked changes), GET /commits/{sha} (the incidents that opened downstream of a deploy, within window_days) and GET /timeline?node_id= (one node's changes, incidents and cost; the cost needs the FinOps module). ATTRIBUTION_CACHE_TTL_SECONDS (60) caches each answer.

Working an incident#

Reading incidents needs no special permission. Changing one needs alerts:manage, which the oncall-sre and tenant-admin role templates include and data-engineer does not.

  • Accept and decline. A machine-created incident starts in the triage stage. Accepting it moves it to active and starts the clock for time to acknowledge. Declining it closes it as not an incident, and it does not count. An incident that recovers by itself while still in triage is normally declined by the system rather than resolved, for the same reason.
  • Acknowledge, resolve, close, reopen. These move status. Each is recorded in the incident's history.
  • Snooze. Quiet one incident, or a whole group.
  • Mark as expected. Flags the incident as a known issue and saves a suppression rule for its source. With INCIDENT_SUPPRESSION_GATE_ENABLED on (it is off by default), an incident that an active permanent rule matches is never created, and the failure is counted on the rule (suppressed_count) instead.
  • Maintenance windows. A window with alert suppression turns the incidents that would open inside it into expected records. Windows are managed under /api/v1/maintenance_windows; changing one needs maintenance:manage.

Unacknowledged incidents that have a team are nudged in the team's channel, after 48 hours by default (INCIDENT_ESCALATION_HOURS_CRITICAL, INCIDENT_ESCALATION_HOURS_HIGH). INCIDENT_ESCALATION_FALLBACK_CHANNEL_ID adds a second channel after INCIDENT_ESCALATION_FALLBACK_AFTER_NUDGES (2) nudges. In a deployment from the reference Terraform, a scheduled janitor (POST /api/v1/alerts/check-incident-aging) closes configuration incidents that have not failed for 7 days and any incident that has not failed for 30 days (a snoozed incident is exempt from the 30-day close), and it alerts on incidents older than 14 days. An incident that keeps failing is never closed this way.

Undo#

Some operations destroy something. Nistor routes them through reversible operations, a time-boxed, audited undo. The operation runs in five steps: capture a recovery artifact, ask for approval if the operation requires it, perform the change, record it, and later restore from the artifact if you ask. Each operation type has a reverser that knows how to capture, restore and purge its artifact. A recorded operation is in one of these states: pending_approval, executed, restored, expired, execute_failed or restore_failed. A failed restore keeps its artifact and can be tried again while the time box holds. When the time box lapses, a sweep purges the artifact and marks the operation expired.

Operation Retention Captured
action_run.delete 30 days The run and its correlated input logs.
incident.acknowledge, incident.close, incident.reopen, incident.snooze 7 days The incident's lifecycle fields. A restore writes back only the fields that transition changed, so it leaves a later, unrelated edit in place.
rule.pause, rule.resume, dq_check.shadow_mode 7 days The prior state.
action_run.replay, chain_step.retry, dq_check.run 7 days The run they spawn. A restore cancels that run.
action_run.cancel none Nothing: cancelling a run cannot be undone, and the record says so.

In this repository, deleting an action run is the operation the backend guards itself. The other reversers serve callers that carry out proposals through the same guard, such as the AI module's investigation agent and the Advisor module. None of the reversers in this repository requires approval; a reverser can require it always, or above a threshold it sets. Approval is a two-person rule: the approver must differ from the requester, and a pending request expires after 24 hours.

Reading the log needs reversible:view, restoring needs reversible:restore, and approving a gated operation needs reversible:approve. Of the role templates, only tenant-admin (which holds every permission) includes them, so an administrator grants them to anyone else. The log is at GET /api/v1/reversible-operations/, a restore is POST /api/v1/reversible-operations/{op_id}/restore (409 if it was already restored, 410 if the time box has lapsed), and the approval gate is under /api/v1/operation-approvals. The kill switch is the backend environment variable REVERSIBLE_OPS_ENABLED. It is on by default; set it to false and a guarded operation runs with no capture and no record.

What needs what#

Feature Needs
Incidents, the kinds listed above, class and tier, root-cause clusters, folding, the lifecycle Open source.
Maintenance windows Open source, with the scheduler capability.
Detectors marked off by default Their setting, named above.
data_quality incidents, and data-quality signals in a data incident The Data quality module.
budget_exceeded and cost_anomaly incidents The FinOps module.
Investigation-agent proposals, and the Google Chat buttons that act on an incident The AI module.
"What changed?" DATA_GIT_BLAME_ENABLED, off by default, and the attribution:view permission.
Undo of operations other than run deletion The module that proposes them: AI or Advisor.

The modules are part of Nistor Enterprise. For the settings, see Configuration.

Edit this page on GitLab

↑ ↓ to moveEnter to openEsc to close