Introduction
After wiring agents to warehouses, OLTP systems, and streams, the next operational bottleneck is trust: catching bad data before it hits dashboards, models, and downstream apps. This article delivers an end-to-end Data Quality & Lineage Incident Response Agent that consumes test results from your quality framework (e.g., dbt tests, Great Expectations, Deequ), correlates failures with lineage, estimates impact, and executes bounded actions—quarantine, backfill, revert, or notify—only when it has a verifiable receipt.
The Use Case
Data teams run hundreds of tests across sources and transformations. When a freshness SLA slips, a null-rate spikes, or a schema breaks, the scramble begins: who owns the table, what depends on it, which dashboards or models are affected, and what safe step should we take now? The agent automates that loop. It ingests quality signals and lineage graphs, ranks the incident by blast radius, proposes a small set of remediations with preconditions, and executes the chosen one through typed tools—each returning a receipt (job run ID, pull request ID, ticket ID, quarantine rule ID). It then posts a concise RCA stub and opens an incident task with acceptance criteria.
Prompt Contract (agent interface)
# file: contracts/data_quality_incident_v1.yaml
role: "DQIncidentAgent"
scope: >
Triage data-quality failures using tests + lineage; propose and execute bounded remediations.
Ask once if critical fields are missing (asset_id, test_id, failure, detected_at, lineage_ctx).
Never claim success without a receipt from the target system.
output:
type: object
required: [summary, severity, impact, proposals, citations, next_steps, tool_proposals]
properties:
summary: {type: string, maxWords: 100}
severity: {type: string, enum: ["low","medium","high","critical"]}
impact:
type: object
required: [downstream_assets, estimated_viewers, data_freshness_min]
properties:
downstream_assets: {type: array, items: string}
estimated_viewers: {type: integer}
data_freshness_min: {type: integer}
proposals:
type: array
items:
type: object
required: [action, reason, preconditions]
properties:
action: {type: string, enum: ["quarantine","backfill","revert_pipeline","pause_refresh","open_ticket","notify_owners"]}
reason: {type: string}
preconditions: {type: array, items: string}
citations: {type: array, items: string} # test/run IDs, lineage edges, policy claims
next_steps: {type: array, items: string, maxItems: 6}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name:
type: string
enum: [GetLineage, GetQualityRun, QuarantineAsset, TriggerBackfill,
RevertPipeline, PauseRefresh, CreateTicket, NotifyOwners, PostRCA]
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "data_quality_policy.v3"
citation_rule: "Minimal-span references to failing_test_id, upstream->downstream edges, and policy clauses."
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel
from typing import Optional, List, Dict
from datetime import datetime
class GetLineageArgs(BaseModel):
asset_id: str
class GetQualityRunArgs(BaseModel):
test_id: str
run_id: Optional[str] = None
class QuarantineAssetArgs(BaseModel):
asset_id: str
rule: str # e.g., filter predicate or blocklist
reason: str
expires_at: Optional[datetime]
class TriggerBackfillArgs(BaseModel):
job_id: str
start: str # ISO date
end: str # ISO date
priority: str = "normal"
class RevertPipelineArgs(BaseModel):
repo: str
environment: str
commit_sha: str
reason: str
class PauseRefreshArgs(BaseModel):
dashboard_id: str
duration_min: int
class CreateTicketArgs(BaseModel):
title: str
description: str
severity: str
owners: List[str]
class NotifyOwnersArgs(BaseModel):
owners: List[str]
message: str
class PostRCAArgs(BaseModel):
asset_id: str
incident_id: str
summary: str
runbook_link: Optional[str]
class ToolReceipt(BaseModel):
tool: str
ok: bool
ref: str # external receipt (run_id, PR id, ticket id)
message: str = ""
data: Optional[Dict] = None
# adapters.py (demo logic—wire to your platform APIs)
from tools import *
LINEAGE = {
"warehouse.sales.orders": ["semantic.sales_model", "dashboard.revenue_core"],
"warehouse.crm.accounts": ["semantic.crm_model", "model.churn_xgb"]
}
OWNERS = {
"warehouse.sales.orders": ["dataops@sales","owner@sales"],
"dashboard.revenue_core": ["bi@sales"]
}
def get_lineage(a: GetLineageArgs) -> ToolReceipt:
downstream = LINEAGE.get(a.asset_id, [])
viewers = 420 if "dashboard" in " ".join(downstream) else 120
return ToolReceipt(tool="GetLineage", ok=True, ref=f"lin-{a.asset_id}",
data={"downstream": downstream, "estimated_viewers": viewers})
def get_quality_run(a: GetQualityRunArgs) -> ToolReceipt:
return ToolReceipt(tool="GetQualityRun", ok=True, ref=f"run-{a.test_id}",
data={"failing_test_id": a.test_id, "null_rate": 0.34, "freshness_min": 185})
def quarantine_asset(a: QuarantineAssetArgs) -> ToolReceipt:
return ToolReceipt(tool="QuarantineAsset", ok=True, ref=f"q-{a.asset_id}", message="Quarantine rule applied",
data={"rule": a.rule})
def trigger_backfill(a: TriggerBackfillArgs) -> ToolReceipt:
return ToolReceipt(tool="TriggerBackfill", ok=True, ref=f"job-{a.job_id}", message="Backfill started")
def revert_pipeline(a: RevertPipelineArgs) -> ToolReceipt:
return ToolReceipt(tool="RevertPipeline", ok=True, ref=f"pr-revert-{a.commit_sha[:7]}", message="Revert PR opened")
def pause_refresh(a: PauseRefreshArgs) -> ToolReceipt:
return ToolReceipt(tool="PauseRefresh", ok=True, ref=f"pause-{a.dashboard_id}", message="Refresh paused")
def create_ticket(a: CreateTicketArgs) -> ToolReceipt:
return ToolReceipt(tool="CreateTicket", ok=True, ref="INC-4821", message="Incident opened")
def notify_owners(a: NotifyOwnersArgs) -> ToolReceipt:
return ToolReceipt(tool="NotifyOwners", ok=True, ref="msg-01", message="Notified owners")
def post_rca(a: PostRCAArgs) -> ToolReceipt:
return ToolReceipt(tool="PostRCA", ok=True, ref=f"rca-{a.incident_id}", message="RCA stub posted")

Join the conversation! Your thoughts help the community grow.