<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Agent Proposes, DQX Disposes in Community Articles</title>
    <link>https://community.databricks.com/t5/community-articles/agent-proposes-dqx-disposes/m-p/169154#M1578</link>
    <description>&lt;P class=""&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN&gt;Every serious data platform grows a dead-letter table, and it always becomes a graveyard. Here is how we turned one into a self-healing queue — without letting a language model anywhere near the warehouse.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Every data platform that takes quality seriously ends up with a dead-letter table. You run pre-commit checks, and the rows that fail don’t go into Silver — they go into quarantine. It is the responsible thing to do. It is also, in practice, where data goes to die.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Nobody wants to open the quarantine table. The rows that land there are, by definition, the annoying ones: a code with a stray alphabetic prefix, a timestamp in the wrong format, a duplicate that got emitted twice, a foreign key that doesn’t quite line up because one side trims whitespace and the other doesn’t. Fixing them is fiddly, one-off work, and the backlog only grows.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;We built a system that closes the loop. An LLM agent triages quarantined rows and proposes fixes. Those fixes are re-validated deterministically against the exact checks that quarantined the row in the first place. Only the rows that pass are merged back into Silver. Everything else escalates to a human review queue instead of looping forever.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The interesting part isn’t the LLM. It’s the constraints around it.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3" color="#333333"&gt;01&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Two design principles&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Everything in the system falls out of two rules we decided not to break.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino" size="4" color="#008080"&gt;“The agent proposes. DQX disposes.”&lt;/FONT&gt;&lt;/EM&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" size="4" color="#008080"&gt;&lt;SPAN class=""&gt;Principle one&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The agent never writes to Silver or Gold. Not once. Every agent-curated row is re-run through the same data-quality engine — we use&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://databrickslabs.github.io/dqx/" target="_blank" rel="noopener"&gt;DQX&lt;/A&gt;, the Databricks Labs quality framework — that quarantined it. If the “fix” doesn’t actually make the row pass the checks, it doesn’t get in. This keeps a hallucinated correction from silently corrupting the warehouse, and it makes the whole thing auditable: the gate is code, not vibes.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;FONT size="4"&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino" color="#008080"&gt;“Documented fixes beat guessed fixes.”&lt;/FONT&gt;&lt;/EM&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" color="#008080"&gt;&lt;SPAN class=""&gt;Principle two&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;If a rule owner has already written down exactly how a violation should be remediated —&lt;/FONT&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;“primary-key collision on customer_id&amp;nbsp;&lt;/FONT&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;&amp;nbsp;keep the row with the latest updated_at&lt;/FONT&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;, drop the rest”&lt;/FONT&gt;&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;— then that fix is applied deterministically and the LLM never sees the row. The agent is the fallback for rules nobody has written a playbook for yet. It is not the default path.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;That second principle matters more than it sounds. The cheapest, most reliable, most auditable model call is the one you never make.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_0-1789792223114.png" style="width: 738px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31299iDBC1C2133FF3C40C/image-dimensions/738x487?v=v2" width="738" height="487" role="button" title="KeerthiItigimat_0-1789792223114.png" alt="KeerthiItigimat_0-1789792223114.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P class="lia-indent-padding-left-60px"&gt;&lt;FONT face="georgia,palatino" size="3" color="#808080"&gt;&lt;STRONG&gt;Figure 1.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;The loop. A quarantined row is fixed by a documented playbook strategy or, failing that, by the agent — then re-validated against the identical DQX checks before anything touches Silver.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;02&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The shape of the pipeline&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The whole thing runs as a Databricks Workflow. Because a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;For Each&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;task can only wrap a single task, not a sub-graph, the per-table pipeline is a child job that the parent fans out to — one child run per quarantine table.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_1-1789792512733.png" style="width: 822px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31300i0E4F45D329131213/image-dimensions/822x446?v=v2" width="822" height="446" role="button" title="KeerthiItigimat_1-1789792512733.png" alt="KeerthiItigimat_1-1789792512733.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT color="#808080"&gt;&lt;FONT face="georgia,palatino"&gt;&lt;STRONG&gt;Figure 2.&lt;/STRONG&gt;The Workflow DAG. Parent discovers and fans out; each child run walks the same seven-stage path. 06 runs on&amp;nbsp;&lt;EM&gt;at least one success&amp;nbsp;&lt;/EM&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino"&gt;of re-ingest or a false gate, so every table produces an audit row either way.&lt;/FONT&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_2-1789792689817.png" style="width: 792px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31301iD16BDC12646955FE/image-dimensions/792x384?v=v2" width="792" height="384" role="button" title="KeerthiItigimat_2-1789792689817.png" alt="KeerthiItigimat_2-1789792689817.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P class="lia-indent-padding-left-30px"&gt;&lt;FONT face="georgia,palatino" color="#808080"&gt;&lt;STRONG&gt;Figure 3.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;A recreation of the parent job’s run graph in Databricks Workflows — discover, then a For Each&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;fan-out to one child run per quarantine table. Table names and durations are illustrative.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Each stage writes only to a control table — a review queue with one row per quarantined row per pass. Nothing touches Silver until the final merge, and that merge only ever sees rows that have already passed re-validation.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;03&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Triage&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Pull the unprocessed rows from one quarantine table, dedupe to the latest quarantined version per row, and flatten the failure metadata. DQX records failures as an array of structs —&lt;SPAN&gt;&amp;nbsp;{name, message, ...}&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— and triage flattens those names into a plain&lt;SPAN&gt;&amp;nbsp;array&amp;lt;string&amp;gt;&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;of violated rule names, then seeds the review queue with status pending.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;It also captures the business key at this moment. Reconstructing a natural key later, after the row has been patched, is guesswork; capturing it at quarantine time is not.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;04&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Playbook remediation — the deterministic half&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;This is where documented fixes get applied. Each dataset has a YAML file — not in the codebase, but in a Unity Catalog Volume, so adding or changing a rule needs no deploy:&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;STRONG&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;remediation_playbooks / silver_orders.yaml&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/DIV&gt;&lt;PRE&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;playbooks:&lt;/SPAN&gt;

  &lt;SPAN class=""&gt;# structurally broken — not a recoverable formatting slip&lt;/SPAN&gt;
  - rule_name: &lt;SPAN class=""&gt;code_not_matching_regex&lt;/SPAN&gt;
    priority: 10
    where: &lt;SPAN class=""&gt;"code rlike '^[A-Za-z]'"&lt;/SPAN&gt;
    strategy: &lt;SPAN class=""&gt;reject_rows&lt;/SPAN&gt;
    params: { reason: &lt;SPAN class=""&gt;"alphabetic prefix on a numeric code"&lt;/SPAN&gt; }

  &lt;SPAN class=""&gt;# documented tie-breaker: keep the most recently updated row&lt;/SPAN&gt;
  - rule_name: &lt;SPAN class=""&gt;struct_customer_id_order_ts_is_not_unique&lt;/SPAN&gt;
    priority: 20
    strategy: &lt;SPAN class=""&gt;dedupe_keep_latest&lt;/SPAN&gt;
    params:
      partition_by: &lt;SPAN class=""&gt;[customer_id, order_ts]&lt;/SPAN&gt;
      order_by: &lt;SPAN class=""&gt;updated_at&lt;/SPAN&gt;
      order_direction: &lt;SPAN class=""&gt;desc&lt;/SPAN&gt;
&lt;/FONT&gt;&lt;/PRE&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Entries run in priority order; the first one that matches a row wins. Three things we learned to bake in:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;where&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;predicates let one rule route by value.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'Duplicate'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;goes to dedupe;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'Invalid'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;has no entry, so it falls through to the agent. Same rule, different handling, driven by the data.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;reject_rows&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is a terminal state, not a failure.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Some rows genuinely can’t be recovered. Dropping them is a decision the rule owner made on purpose, and it is recorded as such — not retried, not escalated, not seen by the agent.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Dedup is configured only here.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;The agent never deduplicates. Choosing which of two near-identical rows survives is a business call with a documented tie-breaker, not something to infer.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN&gt;The strategy&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;functions&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;—&amp;nbsp;&lt;/SPAN&gt;dedupe_keep_latest&lt;SPAN&gt;,&amp;nbsp;&lt;/SPAN&gt;standardize_value&lt;SPAN&gt;,&amp;nbsp;&lt;/SPAN&gt;fill_default&lt;SPAN&gt;,&amp;nbsp;&lt;/SPAN&gt;reject_rows&lt;SPAN&gt;&amp;nbsp;— are code. A new&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;kind&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;of fix needs a deploy. A new&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;instance&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;of an existing kind is just a file upload. Behavior in code, policy in config: that split is what keeps the playbook maintainable by the people who own the rules rather than the people who own the pipeline.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;05&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Agent curation — the fallback&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Only rows with no playbook match reach the agent. And here is the decision that makes this cost effective:&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;FONT color="#008080"&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino"&gt;One model call per distinct violation signature. Not per row.&lt;/FONT&gt;&lt;/EM&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" color="#008080"&gt;&lt;SPAN class=""&gt;The cost model&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;If 40,000 rows all failed the same check on the same column in the same way, that is&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;one&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;call. The model sees the rule name, the check function, the message, a sample of failing values, and up to five passing examples per flagged column. It returns a single deterministic&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;transform&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;per column, chosen from a fixed vocabulary:&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_3-1789793079351.png" style="width: 631px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31302iA354B8B8AAF0110B/image-dimensions/631x191?v=v2" width="631" height="191" role="button" title="KeerthiItigimat_3-1789793079351.png" alt="KeerthiItigimat_3-1789793079351.png" /&gt;&lt;/span&gt;&lt;BR /&gt;&lt;SPAN&gt;Spark then applies that transform to every row in the signature. The model does not see 40,000 rows, does not write 40,000 fixes, and does not get to invent an expression. It picks&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;lower_trim&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;for a casing problem or&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;regexp_replace&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;to strip a known prefix, and that is the entire surface area. Token cost is independent of quarantine volume.&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Transforms can be&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;chained&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— strip a prefix,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;then&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;zero-pad — which covers most of the real formatting damage: typos, column shifts, date formats, casing, padding, regex-strippable junk. And we guard against no-ops: if the model proposes&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;left_pad&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;on a string already at the target length, that is not a fix, and it gets downgraded to an escalation.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;06&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The cross-table probe — no model at all&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Referential-integrity checks — a row in table A must match a row in table B — are common, and the LLM is bad at them. So before the agent is involved, a deterministic probe runs: on a sample, normalize the compared column (trim,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;lower_trim,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;upper_trim) on both sides of the join and check whether the mismatch clears. If a normalization resolves, say, 95% of the sample, propose that transform. If it doesn’t, escalate.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A surprising fraction of “broken foreign keys” are just one side of a join carrying trailing whitespace.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;07&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The confidence gate&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The agent attaches a confidence score to every&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;fix&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;decision. Between curation and application, a gate checks the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;minimum&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;confidence across the batch against a threshold. Above it: proceed to auto-apply. Below it: skip straight to audit, and those rows wait for a human.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;One low-confidence signature holds back the whole batch for that table. We would rather under-automate than merge a shaky fix.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;08&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Apply and re-validate&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;For rows that clear the gate: apply the patch, strip every bookkeeping column, and re-run the row through&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;approved_checks_for(silver_table)&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;same rules table&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that quarantined it. Not a copy of the checks. Not a checks file that might have drifted. The same source. “Re-run the exact checks that quarantined it” is only true if both sides read from one place.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Rows that pass are staged. Rows that fail get&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;retry_count += 1&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and go back into the queue. After three failed attempts a row auto-escalates regardless of confidence. Nothing loops forever.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;09&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Reingest&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A dynamic&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;MERGE INTO&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Silver, keyed on the business key that triage captured, columns intersected with the target schema. Every reingested row is tagged with lineage:&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;agent_curated = true, the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;confidence&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;score, the run id, the timestamp. Downstream consumers who don’t trust agent-curated data can filter it out. That is their call to make, and we give them the column to make it with.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_4-1789793230314.png" style="width: 741px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31303iB5F7AE3D080FB450/image-dimensions/741x302?v=v2" width="741" height="302" role="button" title="KeerthiItigimat_4-1789793230314.png" alt="KeerthiItigimat_4-1789793230314.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;DIV class="lia-indent-padding-left-60px"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;Figure 4.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;The review queue — one row per quarantined row per pass. Terminal states in green/red, human work in amber.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_5-1789793360736.png" style="width: 683px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31304iE003D3B6B1800DF7/image-dimensions/683x279?v=v2" width="683" height="279" role="button" title="KeerthiItigimat_5-1789793360736.png" alt="KeerthiItigimat_5-1789793360736.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;DIV class="lia-indent-padding-left-90px"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;Figure 5.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Illustrative outcome split for a single run. The playbook clears the bulk; the agent handles a couple hundred; a couple dozen reach a human.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;10&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Dry runs&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Because “an LLM will now edit rows that flow into our warehouse” is a sentence that makes people nervous, there are two ways to see what&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;would&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;happen without committing anything.&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;read-only estimate&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that computes the deterministic half for real — match, apply strategy, re-validate — and reports the agent half as a count only. The model is never called. One row per table into a report table, nothing else.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;full dry run&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that runs the whole pipeline, calls the agent, writes the queue and the staging table — but holds the final&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;MERGE&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and instead reports the exact insert/update split against Silver.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Run the first before you turn the system on. Run the second in staging when you want to see precisely what will land.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;11&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;What this bought us&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The quarantine table stopped being a graveyard. On the first real run, the large majority of quarantined rows resolved deterministically — dedup and structural rejects that rule owners had already documented. A few hundred went to the agent. A couple dozen genuinely needed a human, and those are the ones a person should be looking at.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The lesson we keep relearning: the LLM is the least important component. The value is in the scaffolding — the deterministic-first routing, the fixed transform vocabulary, the re-validation gate, the retry cap, the confidence threshold, the dry runs. The agent is a fallback that turns “a human triages every weird row” into “a human triages the genuinely ambiguous ones.”&lt;/FONT&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Agent proposes. DQX disposes. Silver stays clean.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;</description>
    <pubDate>Sat, 19 Sep 2026 05:22:37 GMT</pubDate>
    <dc:creator>KeerthiItigimat</dc:creator>
    <dc:date>2026-09-19T05:22:37Z</dc:date>
    <item>
      <title>Agent Proposes, DQX Disposes</title>
      <link>https://community.databricks.com/t5/community-articles/agent-proposes-dqx-disposes/m-p/169154#M1578</link>
      <description>&lt;P class=""&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN&gt;Every serious data platform grows a dead-letter table, and it always becomes a graveyard. Here is how we turned one into a self-healing queue — without letting a language model anywhere near the warehouse.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Every data platform that takes quality seriously ends up with a dead-letter table. You run pre-commit checks, and the rows that fail don’t go into Silver — they go into quarantine. It is the responsible thing to do. It is also, in practice, where data goes to die.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Nobody wants to open the quarantine table. The rows that land there are, by definition, the annoying ones: a code with a stray alphabetic prefix, a timestamp in the wrong format, a duplicate that got emitted twice, a foreign key that doesn’t quite line up because one side trims whitespace and the other doesn’t. Fixing them is fiddly, one-off work, and the backlog only grows.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;We built a system that closes the loop. An LLM agent triages quarantined rows and proposes fixes. Those fixes are re-validated deterministically against the exact checks that quarantined the row in the first place. Only the rows that pass are merged back into Silver. Everything else escalates to a human review queue instead of looping forever.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The interesting part isn’t the LLM. It’s the constraints around it.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3" color="#333333"&gt;01&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Two design principles&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Everything in the system falls out of two rules we decided not to break.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino" size="4" color="#008080"&gt;“The agent proposes. DQX disposes.”&lt;/FONT&gt;&lt;/EM&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" size="4" color="#008080"&gt;&lt;SPAN class=""&gt;Principle one&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The agent never writes to Silver or Gold. Not once. Every agent-curated row is re-run through the same data-quality engine — we use&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;A href="https://databrickslabs.github.io/dqx/" target="_blank" rel="noopener"&gt;DQX&lt;/A&gt;, the Databricks Labs quality framework — that quarantined it. If the “fix” doesn’t actually make the row pass the checks, it doesn’t get in. This keeps a hallucinated correction from silently corrupting the warehouse, and it makes the whole thing auditable: the gate is code, not vibes.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;FONT size="4"&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino" color="#008080"&gt;“Documented fixes beat guessed fixes.”&lt;/FONT&gt;&lt;/EM&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" color="#008080"&gt;&lt;SPAN class=""&gt;Principle two&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;If a rule owner has already written down exactly how a violation should be remediated —&lt;/FONT&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;“primary-key collision on customer_id&amp;nbsp;&lt;/FONT&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;&amp;nbsp;keep the row with the latest updated_at&lt;/FONT&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;, drop the rest”&lt;/FONT&gt;&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;FONT face="georgia, palatino" color="#333333"&gt;— then that fix is applied deterministically and the LLM never sees the row. The agent is the fallback for rules nobody has written a playbook for yet. It is not the default path.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;That second principle matters more than it sounds. The cheapest, most reliable, most auditable model call is the one you never make.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_0-1789792223114.png" style="width: 738px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31299iDBC1C2133FF3C40C/image-dimensions/738x487?v=v2" width="738" height="487" role="button" title="KeerthiItigimat_0-1789792223114.png" alt="KeerthiItigimat_0-1789792223114.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P class="lia-indent-padding-left-60px"&gt;&lt;FONT face="georgia,palatino" size="3" color="#808080"&gt;&lt;STRONG&gt;Figure 1.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;The loop. A quarantined row is fixed by a documented playbook strategy or, failing that, by the agent — then re-validated against the identical DQX checks before anything touches Silver.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;02&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The shape of the pipeline&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The whole thing runs as a Databricks Workflow. Because a&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;For Each&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;task can only wrap a single task, not a sub-graph, the per-table pipeline is a child job that the parent fans out to — one child run per quarantine table.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_1-1789792512733.png" style="width: 822px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31300i0E4F45D329131213/image-dimensions/822x446?v=v2" width="822" height="446" role="button" title="KeerthiItigimat_1-1789792512733.png" alt="KeerthiItigimat_1-1789792512733.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT color="#808080"&gt;&lt;FONT face="georgia,palatino"&gt;&lt;STRONG&gt;Figure 2.&lt;/STRONG&gt;The Workflow DAG. Parent discovers and fans out; each child run walks the same seven-stage path. 06 runs on&amp;nbsp;&lt;EM&gt;at least one success&amp;nbsp;&lt;/EM&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino"&gt;of re-ingest or a false gate, so every table produces an audit row either way.&lt;/FONT&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;BR /&gt;&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_2-1789792689817.png" style="width: 792px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31301iD16BDC12646955FE/image-dimensions/792x384?v=v2" width="792" height="384" role="button" title="KeerthiItigimat_2-1789792689817.png" alt="KeerthiItigimat_2-1789792689817.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;P class="lia-indent-padding-left-30px"&gt;&lt;FONT face="georgia,palatino" color="#808080"&gt;&lt;STRONG&gt;Figure 3.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;A recreation of the parent job’s run graph in Databricks Workflows — discover, then a For Each&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;fan-out to one child run per quarantine table. Table names and durations are illustrative.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Each stage writes only to a control table — a review queue with one row per quarantined row per pass. Nothing touches Silver until the final merge, and that merge only ever sees rows that have already passed re-validation.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;03&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/FONT&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Triage&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Pull the unprocessed rows from one quarantine table, dedupe to the latest quarantined version per row, and flatten the failure metadata. DQX records failures as an array of structs —&lt;SPAN&gt;&amp;nbsp;{name, message, ...}&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— and triage flattens those names into a plain&lt;SPAN&gt;&amp;nbsp;array&amp;lt;string&amp;gt;&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;of violated rule names, then seeds the review queue with status pending.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;It also captures the business key at this moment. Reconstructing a natural key later, after the row has been patched, is guesswork; capturing it at quarantine time is not.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;04&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Playbook remediation — the deterministic half&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;This is where documented fixes get applied. Each dataset has a YAML file — not in the codebase, but in a Unity Catalog Volume, so adding or changing a rule needs no deploy:&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;DIV class=""&gt;&lt;STRONG&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;remediation_playbooks / silver_orders.yaml&lt;/FONT&gt;&lt;/STRONG&gt;&lt;/DIV&gt;&lt;PRE&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;playbooks:&lt;/SPAN&gt;

  &lt;SPAN class=""&gt;# structurally broken — not a recoverable formatting slip&lt;/SPAN&gt;
  - rule_name: &lt;SPAN class=""&gt;code_not_matching_regex&lt;/SPAN&gt;
    priority: 10
    where: &lt;SPAN class=""&gt;"code rlike '^[A-Za-z]'"&lt;/SPAN&gt;
    strategy: &lt;SPAN class=""&gt;reject_rows&lt;/SPAN&gt;
    params: { reason: &lt;SPAN class=""&gt;"alphabetic prefix on a numeric code"&lt;/SPAN&gt; }

  &lt;SPAN class=""&gt;# documented tie-breaker: keep the most recently updated row&lt;/SPAN&gt;
  - rule_name: &lt;SPAN class=""&gt;struct_customer_id_order_ts_is_not_unique&lt;/SPAN&gt;
    priority: 20
    strategy: &lt;SPAN class=""&gt;dedupe_keep_latest&lt;/SPAN&gt;
    params:
      partition_by: &lt;SPAN class=""&gt;[customer_id, order_ts]&lt;/SPAN&gt;
      order_by: &lt;SPAN class=""&gt;updated_at&lt;/SPAN&gt;
      order_direction: &lt;SPAN class=""&gt;desc&lt;/SPAN&gt;
&lt;/FONT&gt;&lt;/PRE&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Entries run in priority order; the first one that matches a row wins. Three things we learned to bake in:&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;where&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;predicates let one rule route by value.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'Duplicate'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;goes to dedupe;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;status = 'Invalid'&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;has no entry, so it falls through to the agent. Same rule, different handling, driven by the data.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;reject_rows&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;is a terminal state, not a failure.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Some rows genuinely can’t be recovered. Dropping them is a decision the rule owner made on purpose, and it is recorded as such — not retried, not escalated, not seen by the agent.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;STRONG&gt;Dedup is configured only here.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;The agent never deduplicates. Choosing which of two near-identical rows survives is a business call with a documented tie-breaker, not something to infer.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN&gt;The strategy&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;functions&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;—&amp;nbsp;&lt;/SPAN&gt;dedupe_keep_latest&lt;SPAN&gt;,&amp;nbsp;&lt;/SPAN&gt;standardize_value&lt;SPAN&gt;,&amp;nbsp;&lt;/SPAN&gt;fill_default&lt;SPAN&gt;,&amp;nbsp;&lt;/SPAN&gt;reject_rows&lt;SPAN&gt;&amp;nbsp;— are code. A new&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;kind&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;of fix needs a deploy. A new&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;instance&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;of an existing kind is just a file upload. Behavior in code, policy in config: that split is what keeps the playbook maintainable by the people who own the rules rather than the people who own the pipeline.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;05&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Agent curation — the fallback&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Only rows with no playbook match reach the agent. And here is the decision that makes this cost effective:&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;P&gt;&lt;FONT color="#008080"&gt;&lt;EM&gt;&lt;FONT face="georgia,palatino"&gt;One model call per distinct violation signature. Not per row.&lt;/FONT&gt;&lt;/EM&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;FONT face="georgia,palatino" color="#008080"&gt;&lt;SPAN class=""&gt;The cost model&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;If 40,000 rows all failed the same check on the same column in the same way, that is&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;one&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;call. The model sees the rule name, the check function, the message, a sample of failing values, and up to five passing examples per flagged column. It returns a single deterministic&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;transform&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;per column, chosen from a fixed vocabulary:&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_3-1789793079351.png" style="width: 631px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31302iA354B8B8AAF0110B/image-dimensions/631x191?v=v2" width="631" height="191" role="button" title="KeerthiItigimat_3-1789793079351.png" alt="KeerthiItigimat_3-1789793079351.png" /&gt;&lt;/span&gt;&lt;BR /&gt;&lt;SPAN&gt;Spark then applies that transform to every row in the signature. The model does not see 40,000 rows, does not write 40,000 fixes, and does not get to invent an expression. It picks&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;lower_trim&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;for a casing problem or&lt;/SPAN&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;regexp_replace&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;to strip a known prefix, and that is the entire surface area. Token cost is independent of quarantine volume.&lt;/SPAN&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Transforms can be&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;chained&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— strip a prefix,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;then&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;zero-pad — which covers most of the real formatting damage: typos, column shifts, date formats, casing, padding, regex-strippable junk. And we guard against no-ops: if the model proposes&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;left_pad&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;on a string already at the target length, that is not a fix, and it gets downgraded to an escalation.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;06&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The cross-table probe — no model at all&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Referential-integrity checks — a row in table A must match a row in table B — are common, and the LLM is bad at them. So before the agent is involved, a deterministic probe runs: on a sample, normalize the compared column (trim,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;lower_trim,&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;upper_trim) on both sides of the join and check whether the mismatch clears. If a normalization resolves, say, 95% of the sample, propose that transform. If it doesn’t, escalate.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A surprising fraction of “broken foreign keys” are just one side of a join carrying trailing whitespace.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;07&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;The confidence gate&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The agent attaches a confidence score to every&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;fix&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;decision. Between curation and application, a gate checks the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;minimum&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;confidence across the batch against a threshold. Above it: proceed to auto-apply. Below it: skip straight to audit, and those rows wait for a human.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;One low-confidence signature holds back the whole batch for that table. We would rather under-automate than merge a shaky fix.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;08&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Apply and re-validate&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;For rows that clear the gate: apply the patch, strip every bookkeeping column, and re-run the row through&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;approved_checks_for(silver_table)&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;— the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;same rules table&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that quarantined it. Not a copy of the checks. Not a checks file that might have drifted. The same source. “Re-run the exact checks that quarantined it” is only true if both sides read from one place.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Rows that pass are staged. Rows that fail get&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;retry_count += 1&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and go back into the queue. After three failed attempts a row auto-escalates regardless of confidence. Nothing loops forever.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;09&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Reingest&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A dynamic&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;MERGE INTO&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;Silver, keyed on the business key that triage captured, columns intersected with the target schema. Every reingested row is tagged with lineage:&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;agent_curated = true, the&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;confidence&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;score, the run id, the timestamp. Downstream consumers who don’t trust agent-curated data can filter it out. That is their call to make, and we give them the column to make it with.&lt;/FONT&gt;&lt;/P&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_4-1789793230314.png" style="width: 741px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31303iB5F7AE3D080FB450/image-dimensions/741x302?v=v2" width="741" height="302" role="button" title="KeerthiItigimat_4-1789793230314.png" alt="KeerthiItigimat_4-1789793230314.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;DIV class="lia-indent-padding-left-60px"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;Figure 4.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;The review queue — one row per quarantined row per pass. Terminal states in green/red, human work in amber.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;DIV class=""&gt;&amp;nbsp;&lt;/DIV&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-center" image-alt="KeerthiItigimat_5-1789793360736.png" style="width: 683px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/31304iE003D3B6B1800DF7/image-dimensions/683x279?v=v2" width="683" height="279" role="button" title="KeerthiItigimat_5-1789793360736.png" alt="KeerthiItigimat_5-1789793360736.png" /&gt;&lt;/span&gt;&lt;/DIV&gt;&lt;DIV class="lia-indent-padding-left-90px"&gt;&lt;FONT color="#808080"&gt;&lt;STRONG&gt;Figure 5.&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN&gt;Illustrative outcome split for a single run. The playbook clears the bulk; the agent handles a couple hundred; a couple dozen reach a human.&lt;/SPAN&gt;&lt;/FONT&gt;&lt;/DIV&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;10&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;Dry runs&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Because “an LLM will now edit rows that flow into our warehouse” is a sentence that makes people nervous, there are two ways to see what&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;EM&gt;would&lt;/EM&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;happen without committing anything.&lt;/FONT&gt;&lt;/P&gt;&lt;UL&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;read-only estimate&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that computes the deterministic half for real — match, apply strategy, re-validate — and reports the agent half as a count only. The model is never called. One row per table into a report table, nothing else.&lt;/FONT&gt;&lt;/LI&gt;&lt;LI&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;A&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;full dry run&lt;/STRONG&gt;&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;that runs the whole pipeline, calls the agent, writes the queue and the staging table — but holds the final&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;MERGE&lt;SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;and instead reports the exact insert/update split against Silver.&lt;/FONT&gt;&lt;/LI&gt;&lt;/UL&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Run the first before you turn the system on. Run the second in staging when you want to see precisely what will land.&lt;/FONT&gt;&lt;/P&gt;&lt;H2&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;&lt;SPAN class=""&gt;&lt;FONT size="3"&gt;11&lt;/FONT&gt;&lt;BR /&gt;&lt;/SPAN&gt;What this bought us&lt;/FONT&gt;&lt;/H2&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The quarantine table stopped being a graveyard. On the first real run, the large majority of quarantined rows resolved deterministically — dedup and structural rejects that rule owners had already documented. A few hundred went to the agent. A couple dozen genuinely needed a human, and those are the ones a person should be looking at.&lt;/FONT&gt;&lt;/P&gt;&lt;P&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;The lesson we keep relearning: the LLM is the least important component. The value is in the scaffolding — the deterministic-first routing, the fixed transform vocabulary, the re-validation gate, the retry cap, the confidence threshold, the dry runs. The agent is a fallback that turns “a human triages every weird row” into “a human triages the genuinely ambiguous ones.”&lt;/FONT&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;FONT face="georgia,palatino" color="#333333"&gt;Agent proposes. DQX disposes. Silver stays clean.&lt;BR /&gt;&lt;/FONT&gt;&lt;/P&gt;</description>
      <pubDate>Sat, 19 Sep 2026 05:22:37 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/agent-proposes-dqx-disposes/m-p/169154#M1578</guid>
      <dc:creator>KeerthiItigimat</dc:creator>
      <dc:date>2026-09-19T05:22:37Z</dc:date>
    </item>
  </channel>
</rss>

