Praxis — the evidence-bound remediation agent for DataHub-governed data products
Prove it before you propose it.
The gap
Ask any catalog agent about a schema change and it will give you a good paragraph. Ask it to fix the break and you get generated code you cannot trust, because nothing connects the code to the metadata that justified it.
So the expensive incident stays manual. A source column is renamed. Nothing breaks loudly. A dbt model keeps projecting the old field, a production ML model keeps scoring on stale features, and a business-critical dashboard keeps looking fine — until someone notices, three weeks later, by hand.
What Praxis does
Praxis takes that one narrow, expensive incident all the way to a mergeable fix, and refuses to hand you anything it cannot prove.
It reads the exact DataHub schema and lineage for a specific dataset URN, inspects the real dbt files in your repository, generates the smallest safe compatibility patch, verifies it in isolation, stops for a named human decision, re-verifies after that decision, and then publishes only the outputs an external system actually confirmed.
The differentiator is not another incident summary. It is a proof-carrying change set. Every candidate ships with DataHub evidence URNs, before/after SHA-256 hashes of every file, the full verification result including the checks that did not run, and a rollback instruction.
The run, end to end
1 · Exact coordinates, not a search. Praxis starts from a dataset URN, a
missing field, and an observed replacement. DataHub returns the observed
schemaMetadata, ownership, the description, the failed assertion, and — this
is the part that matters — a column-level lineage path proving the old field
is still projected downstream. Entity lineage is never accepted as column proof.
2 · Blast radius. Entity lineage establishes the wider impact: a dbt staging model, a Feast feature table, an MLflow model, its SageMaker deployment, and a critical Looker queue. The risk score is composed only from evidence terms — failed assertion, confirmed column path, production entities, criticality, usage, missing ownership. Every term traces back to a URN.
3 · Generation against real files. Praxis reads your actual dbt model and
contract, hashes both, and finds exactly one safe projection to repair. The
patch is one line — alias the new column back to the contracted name, with the
cast type read from the contract's declared data_type — plus switching
contract: enforced: true on so dbt rejects the next drift itself. It refuses
on ambiguous SQL, a missing contract field, an absent data type, an oversized
file, or any path outside the configured repository.
4 · Fail-closed verification. Seven required checks: the rename is proved by
observed schema; a column path is confirmed; the patch is minimal and preserves
every projected output; there is no destructive SQL; the enforced contract
matches every projected column exactly; the provenance manifest is internally
consistent; the repository inputs still hash the same. Validation runs against a
temporary copy. When dbt is installed, a real dbt parse runs as an eighth
required check and the tier becomes dbt_verified; when it is absent the check
is recorded as skipped, never as passed. If a required check fails, there is
no approval button.
5 · The human gate. Approval requires a named identity and is bound to the exact candidate hashes. On approval Praxis re-reads the repository, repeats verification, and compares the hashes again — so a review that went stale while the human was deciding can never be applied. The source repository is never overwritten.
6 · Confirmed outputs only. With GitHub configured, one Git tree and one
commit become one draft pull request. With DataHub configured,
praxis.verified-impact is applied only to URNs DataHub acknowledged in its
mutation response, and a hash-bound memory note is appended to the source
dataset so the next engineer — or the next agent — inherits what this run
proved. An attempted write is never reported as a successful graph delta; a
connector failure produces a partial result naming the exact confirmed subset.
7 · Tamper-evident by construction. Every handoff lands in a SHA-256 chained
ledger in SQLite: event_hash = SHA256(canonical_event ‖ sequence ‖ prev_hash),
with a unique (run_id, sequence) index to prevent reordering. The UI verifies
the chain on every snapshot. Startup recovers queued, running, applying, and
approved-waiting runs; apply is safe to resume because branches and memory notes
use stable run identifiers.
Try it in 60 seconds — no credentials
python -m praxis # Python 3.10+, zero dependencies
Open http://127.0.0.1:8787, select Resolve evidence & verify, inspect the Evidence and Candidate tabs, and approve or reject.
The backend loads a packaged DataHub-shaped
incident fixture, reads the intentionally broken dbt project under
sandbox/dbt_project, generates new files, validates them, persists a chained
ledger, and serves the resulting artifacts. Results are computed, not replayed.
For the same real dbt parse gate used in the Docker image:
pip install -r requirements-dbt.txt && python -m praxis
Or fully self-contained, including the pinned DataHub SDK:
docker compose up --build
How Praxis uses DataHub
Not as a lookup table — as both the source and the destination of governed context.
| Purpose | DataHub surface |
|---|---|
| Exact observed schema, ownership, description | GraphQL dataset { schemaMetadata, ownership, editableProperties } |
| Wider entity blast radius | GraphQL scrollAcrossLineage with degree filters |
| Column-level field paths (the proof) | Official Python SDK client.lineage.get_lineage(source_column=…) |
| ML deployment edges invisible to lineage search | DataHubGraph.get_aspect(urn, MLModelPropertiesClass) |
| Publishing verified impact | GraphQL addTag mutation, confirmed per URN |
| Publishing organizational memory | GraphQL updateDescription, appending without clobbering |
| Discovery and lineage inside an agent loop | DataHub MCP Server, composed with ours in .mcp.json |
| Agent-facing catalog workflow | DataHub Skill in contrib/skills/, upstream format |
| Reproducible live test graph | datahub docker quickstart --version v1.6.0 + our own seed script |
One command to the live path
The sandbox is a fixture and says so. The DataHub-backed path is one command:
python setup/live_quickstart.py
That installs the pinned SDK, brings up DataHub Core 1.6.0, waits for GMS to answer
GraphQL, and seeds a self-contained nine-entity graph — post-rename source schema,
two-hop column lineage, three ML features, a production model, its model group, its
SageMaker deployment and a critical dashboard. No warehouse, no datapack, no
credentials. --serve starts Praxis on it; --stop tears it down.
After that, every schema field, owner, lineage edge and column path Praxis shows came out of GMS over the wire.
Sample outputs
Four inspectable artifacts per run, checked in at
examples/verified-schema-drift/ with a walkthrough in
examples/README.md:
- the one-line dbt compatibility repair;
- the dbt contract with enforcement enabled;
- a provenance manifest with DataHub URNs and input/output hashes;
- a complete verification report, including skipped checks.
All four are generated by the same code path a judge runs locally.
What is built
Verified end to end. DataHub Core 1.6.0 live schema, ownership, two-hop
column lineage, and a nine-entity dataset-to-dashboard/model/deployment blast
radius. A full live approval created nine confirmed DataHub tags and one
hash-bound memory note with zero write errors. A Dockerized run completed the
combined transaction: two confirmed column paths, nine impact entities, a real
dbt parse, nine confirmed tags, one memory note, and an atomic draft pull
request with exactly one commit and four expected files — with the base branch
SHA and the local checkout unchanged. A contaminated cross-platform dbt
partial-parse cache correctly failed closed before approval.
Limitations. Praxis supports a conservative dbt schema-rename repair,
not arbitrary incident classes. SQL generation requires exactly one bare
projection and an existing contract field with a declared data_type. The
packaged healthcare graph is a test fixture derived from the contest scenario,
not a captured live response. Live permissions depend on your deployment; the
validated target is DataHub Core 1.6.0. Praxis creates a draft PR — it never
merges, never deploys, and never changes production traffic. Production risk
remains until your team merges. The product UI says this on its own front page.
Where the agent is, and where the proof is
Praxis is two layers, and the split is the design.
The agent layer is where language models belong. Praxis ships as an MCP
server (python -m praxis.mcp, five tools) and as a DataHub Skill, so a real
agent — Claude Code, Cursor, Codex, an SDK agent — does the open-ended work it is
good at: finding the dataset, reading the graph, judging whether an incident is
worth acting on, explaining the change to a human. contrib/skills/ pairs the
DataHub MCP Server for discovery with the Praxis MCP server for remediation, and
.mcp.json in the repo wires both up.
The remediation layer is deterministic, and that is what makes it trustworthy. Once the agent has named an exact incident, generation and verification are evidence-constrained code, not inference. Every proposed token traces to source metadata or repository content; there is no prompt-injection path from catalog text into generated production SQL; and the judge path reproduces with zero secrets.
That boundary is enforced, not just described. The MCP tools let an agent prepare,
inspect and explain — and refuse to let it approve.
praxis_approve_remediation requires a named human identity plus an explicit
human_approved flag the tool description forbids an agent from setting on a
person's behalf, and a run with a refused approval stays in waiting with nothing
applied. Tests assert exactly that.
The trade: this narrows the incident classes Praxis handles to ones a deterministic generator can prove. We would rather ship one repair that carries proof than ten that carry prose.
Built and tested
Python 3.10+ with zero runtime dependencies for the sandbox path — stdlib
http.server, sqlite3, urllib, hashlib. Vanilla HTML/CSS/JS frontend, no
build step. Optional pinned integrations: acryl-datahub==1.6.0.15, dbt Core
1.12.0 + dbt-duckdb 1.10.1. Non-root Docker image. Apache 2.0.
Automated tests cover the DataHub 1.6 GraphQL query shapes, live evidence
merging column proof with entity impact, two approval races — at worker handoff,
and an approval landing while the preparation worker still holds the run's slot —
restart-safe state, rejection safety, candidate generation, path traversal,
SQL injection in field inputs, API authorization and the approval token,
SQLite old-schema migration, connector truthfulness (a disabled GitHub raises
instead of fabricating a PR), description-append preservation, and hash-chain
tamper detection. CI runs the suite on Python 3.10 and 3.12, plus a judge-path
job that installs dbt, runs dbt parse on the sandbox project, builds a wheel,
and builds the Docker image.
What we would build next
Broaden the incident taxonomy beyond schema rename — type widening, nullability changes, and partition-key drift are the same transaction shape with a different generator. Then a batch mode that proves and bundles a whole day of drift into one reviewable PR.
Built With
- acryl-datahub
- css3
- datahub
- dbt
- docker
- duckdb
- feast
- github-actions
- github-api
- graphql
- html5
- javascript
- looker
- mlflow
- python
- rest-api
- sagemaker
- snowflake
- sqlite
- threading
- typescript
- unittest
Log in or sign up for Devpost to join the conversation.