Desired-state reconciliation for DataHub
Your catalog knows what is.
Upstream decides what should be — and closes the gap.
You declare the estate you want in one file. Controllers read the DataHub graph through DataHub's own Skills, reason over cited evidence and past incidents, price every change before proposing it, and — only with your approval — write what they learned back into DataHub.
Apache-2.0 · runs against the DataHub OSS quickstart · bring your own model key
tiers: - name: tier-1 match: - domains: ["Data Platform Team"] - name_patterns: ["fct_*", "dim_*"] platforms: ["dbt"] requirements: require_owner: true require_glossary_term: true min_assertions: 1 freshness_sla_hours: 24 max_open_incident_hours: 4 policies: actuation: ratify # never silent max_proposals_per_run: 25 reliability: min_confidence_to_remediate: 0.75 immunize_on_resolve: true
Selectors resolve to URNs through DataHub search, so the spec follows the estate as it grows instead of naming assets one by one.
DataHub is the system of record. Upstream is the system of intelligence.
Nothing here replaces the catalog. Upstream reads it, decides what the metadata means, and returns the conclusion to the same graph — so the value lands in the product your team already opens.
DataHub
Stores what is
The metadata graph, and the retrieval surface Upstream is built on. Every read goes through it.
Upstream
Decides what should be
A declared spec, controllers that reconcile toward it, and a human gate in front of every write.
Thirty-one lines is the entire configuration surface.
Three tiers, each with the requirements it must meet. Everything else is inferred from your graph. A requirement is a pure function over the metadata — no model participates in scoring, so the same estate always scores the same number.
tier-1
Zero tolerance
Order-entry analytics the business reports on. Owner, description, glossary term, governance properties, an assertion, a 24-hour freshness SLA, and no incident open longer than four hours.
tier-2
Owned and column-tested
Staging and intermediate dbt models. Owned, described, and carrying at least one DATASET_COLUMN assertion — the kind that catches a bad value, not just a late table.
raw
The vendor boundary
Contracted and monitored, or drift arrives silently. A schema contract here is what turns a vendor's rename from a quiet null into a failing check.
The gap, as one number that moves.
162 checks across 26 assets, scored by check rather than by asset so the dial moves smoothly as gaps close. This is the real report from examples/conformance/ — before the reconciliation run, and after it.
0%
Estate conformance
+32.7% · 53 checks fixed
The raw tier scores 8% because the vendor feed boundary carries no schema contract, no owner and no test. That is not a scoring artefact — it is the reason the drift below propagates silently.
Measuring drift is a report. Closing it is the product.
Every proposal is grounded and priced before anyone sees it. An owner comes from git history, the lineage graph, a sibling asset or the domain — never from a guess. A description is written from the columns and upstreams the read path already returned. Proposals with no simulated benefit are dropped rather than shown.
observe
50%
Score the graph against the spec. Deterministic, no model.
plan
45
Proposals, each grounded in graph facts and priced by simulation.
ratify
human
A real pause — LangGraph interrupt() with a checkpointer.
actuate
45
Writes, each logged to a ledger that make reset can undo.
verify
83%
Re-score against the same checks. The claim is measured, not asserted.
Projected +32.7% → measured +32.7%
The what-if and the what-is run through the same checks.py over the same context object, so the number on the ratify screen is the number you get. A disagreement here is a bug, not a rounding difference.
A vendor renamed a column. No job failed.
A staging cast turned the revenue mart 85% NULL and every dashboard kept rendering. DataHub's Timeline API reports the rename as a single MODIFY event — not a REMOVE plus an ADD. That one event is the whole case, and the correlator is built on it.
Every conclusion is one click from the graph fact under it.
What Upstream concluded is set in sans. What it quoted from DataHub is mono, and blue. A hypothesis that cites no evidence is dropped in code, before it reaches a person.
The confidence number is computed, never reported by the model.
Five orthogonal signals, one weighted sum, plus a hard gate: a hypothesis with no timeline event, query result or run record behind it is multiplied by 0.6, because a story without a hard artifact is a story. A judge who distrusts language models can still audit this number.
and eight more, for priority
Risk is arithmetic too
business criticality 0.20 · assertion health gap 0.15 · historical fragility 0.15 · lineage depth 0.10 · governance violations 0.10 · incident similarity 0.10 · business impact 0.10 · controller uncertainty 0.10. One dict, one explain(), no model.
bounded by policy
A run cannot spin
Six lineage hops, six forensic queries, one bounded retry, 25 proposals per run. The budget is printed in the trace as it is consumed, and the limits live in the spec where a reviewer can read them without reading the code.
Not knowing is a result, and it is written like one.
Run the same command against a healthy asset and it does not manufacture a story. It stops at 0.70, claims nothing, and names the single probe that would settle the question. Abstention is a first-class outcome here, not an error state.
## No cause claimed The evidence did not support a conclusion above the remediation threshold, so none is asserted. Next probe: Check the ETL job logs for order_details to dim_customers transformation around 2026-08-05 17:28 UTC to confirm mapping failure. confidence 0.70 · below remediate 0.75 · 20 pieces of evidence
Twenty pieces of evidence and no conclusion. An agent that filled this in would be guessing into a catalog people trust.
What it leaves behind, inside DataHub.
Conclusions are not stored in Upstream. They are written back into the catalog your team already opens — so the second failure of this class is cheaper than the first, and the immunization assertions mean there may not be a second run at all.
FIELD_NOT_NULL discount_amount · urn:li:assertion:upstream-ada7c9fd815493a0ac50c5a0 written
before any of it was written
A patch, not just a paragraph
Two files changed — the staging cast and a contract test — git apply --check confirming it applies cleanly, and 13/13 tests passing against the patched model. The diff is committed under examples/pull-requests/. Upstream proposes the fix; it does not merge it.
10/10 write-backs
Every write is reversible
Each one is appended to a ledger with its previous value, so make reset returns the estate to its seeded state. A demo you cannot re-run is a demo you cannot trust.
An operator surface where the frontend computes nothing.
Five screens over a live HTTP API. Filtering, ordering, paging, counting, pricing and even the empty-state copy are backend projections — the same functions that generate the committed artifacts, so the live payload and the recorded one cannot drift. React renders what it is handed and decides nothing.
route
1 line
Validates and delegates. A second statement is logic in the wrong layer.
controller
decides
What a verdict means, read off the record rather than the request.
writer
one table
The same dispatch the controller calls at its own gate.
receipt
typed
What was written, to which aspect, and the urn DataHub returned.
stream
verbatim
The stored receipt, sent byte for byte. Resumable by sequence.
One schema for the write path and the live one
Every mutation answers with a MutationReceipt. That same object is the line appended to the journal and the frame GET /api/events broadcasts — so there is no second schema and no translation layer. A receipt names the screens it invalidated, and each one re-asks its projector rather than patching state by hand.
approving is not writing
And the receipt says which
A proposal is written if it carries a drafted payload and the estate policy allows it. Otherwise the decision is recorded, naming the reason — a draft pull request Upstream never merges, a row read from a report that kept no payload, or a proposal already decided.
partial is a real answer
200, never 500
DataHub has no transaction across aspects, so a batch is not atomic. A half-written batch reports which writes landed and which failed — discarding the successful ones is the single outcome an operator cannot recover from.
Every fact reaches the agent through DataHub's own Skills.
Eight skills over five distinct tools, executed against a live OSS quickstart and read back. When a tool is unavailable the SDK fallback is used and logged — the trace prints which facts arrived the long way, because a read path that hides its own failures is not a read path.
asset-search
search · 43 ms · 10 results
available
lineage-retrieval
get_lineage · 336 ms · upstreams
available
assertion-retrieval
get_dataset_assertions · 32 ms
available
quality-retrieval
list_schema_fields · 77 ms · 18 fields
available
governance-retrieval
get_entities · structuredProperties, health
available
glossary-retrieval
get_entities · glossaryTerms
available
documentation-retrieval
get_entities · description
available
ownership-retrieval
get_entities · empty on this asset
empty, reported
contributed back
reconcile-estate, as a DataHub Skill
The conformance scorer is packaged as a Skill any DataHub agent can call: give it a spec, get back the ranked, remediable gap. It reads. It changes nothing.
reproducible
Every model call is on a cassette
LLM_MODE=replay reproduces a run with no API key and no network. Replay reproduces a run, not a question — a re-seeded estate asks something different, and says so rather than pretending.
What it refuses to do.
The restraint is the feature. Anything that would put a guess into a catalog people trust is left for a human, and named.
violations survive the run, on purpose
16 freshness requirements need an evaluation, not a declaration. 8 assets have no owner anywhere in the graph to infer from. 4 ask for a judgement — a business term, a retention period — that belongs to a person. Each one is listed with its reason.
unapproved writes, by design
The gate is a real pause, not a confirmation dialog: LangGraph interrupt() with a checkpointer, and a test that fails if it ever becomes a pass-through.
models involved in scoring
Conformance, risk and confidence are pure functions over graph facts. The model writes prose and proposes hypotheses; it never reports a number that decides whether Upstream may act.
bounded retry, then it stops
One more forensic pass is allowed when the evidence is thin. After that the run abstains and names the probe a human should take. A stubborn failure cannot turn into an unbounded loop.
The whole demo, from empty to red dial to green.
Python 3.11, Docker with 8 GB, one model key. The estate is DataHub's own sample data — loaded, mirrored into DuckDB for forensic SQL, then broken on purpose by a deterministic scenario library.
Every other agent reads your catalog. Upstream leaves it healthier than it found it.