Decision records for DataHub agents

An agent leaves a note saying it was safe to delete a column. Months later the note is still there, and nobody can tell whether it was ever true.

The note said nothing was using this table. But what a company knows about its own data changes constantly — something may have started using that table an hour before the agent looked, or an hour after. The note doesn't say which version of the world it was describing. So nobody can tell whether the agent was wrong, or whether it was right about a world that had already moved.

That is the whole problem this project fixes. Every time the agent looks something up, we record exactly which version of the company's records it saw — not just what it saw. The note now reads “nothing was using it, according to version 461, recorded at 22:24:56”, and the question is answerable. And when we cannot establish which version the agent saw, we say so and refuse to vouch for the decision, rather than issuing a clean-looking approval.

A record that cannot name the revision it was made against is not a record of a decision.

Start here

Built for Build with DataHub — The Agent Hackathon, track Agents That Do Real Work.


The words this page uses

DataHub vocabulary in one place, so nothing below depends on knowing it already.


The same call, decided both ways

The agent asks DataHub for the downstream lineage of a Snowflake table, and drops a column only if nothing reads from it. Between these two runs a pipeline change wires a consumer to that table. Same agent, same call, opposite outcomes — and each record names the aspect revision it was made against.

TIMELINE — one agent, one question, asked twice

  #0  Read downstream lineage for b2fd91.order_entry_db.order_entry.orders — through the real MCP server
  #1  Bound that read to upstreamLineage v941 (lastObserved=1786026377925)
  #2  Certified C2 — every deciding read named a revision
  #3  DECISION: admit — nothing reads promo_code at v941
  #4  Proposed, NOT applied: DROP COLUMN promo_code

  ···  the world moves: a pipeline change rewires who reads b2fd91.order_entry_db.order_entry.orders

  #5  The identical call, seconds later, same MCP server — b2fd91.order_entry_db.order_entry.orders
  #6  Bound that read to upstreamLineage v942 (lastObserved=1786026380223)
  #7  Certified C2 — every deciding read named a revision
  #8  DECISION: reject — a consumer reads promo_code at v942
  #9  Proposed, NOT applied: DROP COLUMN promo_code

  Same agent. Same call. Opposite decisions.
  Neither log can tell you which world it was made in. Both certificates can.


CERTIFICATES

=== as the agent saw it ===
outcome:  admit
revision: v941  (lastObserved=1786026377925)
Capability class: C2
proposed change:
  -- No downstream consumer reads b2fd91.order_entry_db.order_entry.orders.promo_code at the bound revision.
  -- proposed — not applied by dhdr
  ALTER TABLE b2fd91.order_entry_db.order_entry.orders DROP COLUMN promo_code;

=== as it actually was ===
outcome:  reject
revision: v942  (lastObserved=1786026380223)
Capability class: C2
proposed change:
  -- b2fd91.order_entry_db.analytics.order_history still reads b2fd91.order_entry_db.order_entry.orders at the bound revision, so the drop is refused and deprecation proposed instead.
  -- proposed — not applied by dhdr
  COMMENT ON COLUMN b2fd91.order_entry_db.order_entry.orders.promo_code IS 'deprecated: still read by b2fd91.order_entry_db.analytics.order_history; do not drop until that dependency is removed';

Both certificates say C2, so both decisions are sound. That is the point: neither run was wrong. They saw different worlds, and only the certificate can tell you which.

Through the real MCP server

The run above reads DataHub's aspect API, which can be pointed at a past instant. This one goes through mcp-server-datahub itself, which only ever answers about now — so the world is moved between two live reads instead.

=== no consumer yet ===
tool:         get_lineage (via mcp-server-datahub)
value_source: mcp
outcome:      admit
revision:     v939
Capability class: C2

=== consumer wired up ===
tool:         get_lineage (via mcp-server-datahub)
value_source: mcp
outcome:      reject
revision:     v940
Capability class: C2

The agent made the identical MCP call both times.
value_source=mcp means it decided on the protocol response, not a re-fetch.

value_source: mcp is load-bearing. It means the agent decided on the protocol response it actually received, not on a value re-fetched afterwards. Between two fetches the metadata can move, and then the revision on the record is not the revision that decided.

What "Capability class: C2" means

Every certificate above reports a capability class, never a score. The classes are ordinal — each one is the previous plus a stronger question the record can answer:

classthe record supports asking…
C0can this decision be reproduced identically? (identity replay)
C1…and would a stricter policy have changed the outcome?
C2…and would a looser one? This is what a bound, complete record earns.
C3…and what about decisions whose own writes change what later decisions read? Never certified — reported as the boundary where deductive evidence stops.
nonea deciding read could not be tied to a revision, so nothing is certifiable. Not “C2 with a warning”.

So C2 above means: the record carries enough evidence to ask what this agent would have decided under a different policy, bounded to the revision the read was bound to. It does not mean the decision was correct — dhdr does not adjudicate the drop, only whether the record supports asking about it later.

A percentage is deliberately never produced. A score averaged over incommensurable kinds of missing evidence manufactures exactly the false confidence this exists to prevent.

What the next reader inherits

A decision that is not published is not inherited. Each certificate is written back to the dataset's institutionalMemory, so the next agent or engineer finds it without rerunning anything.

{
  "elements": [
    {
      "createStamp": {
        "actor": "urn:li:corpuser:datahub",
        "time": 1786026360741
      },
      "description": "decision=admit against v937 \u2014 class C2",
      "url": "https://laolex.github.io/datahub-decision-records/certs/1786026360741.json"
    },
    {
      "createStamp": {
        "actor": "urn:li:corpuser:datahub",
        "time": 1786026366051
      },
      "description": "decision=reject against v938 \u2014 class C2",
      "url": "https://laolex.github.io/datahub-decision-records/certs/1786026366051.json"
    }
  ]
}

Which part of the record is load-bearing

Remove one captured field at a time and see what the certifier can still claim. An entry that cannot show which part of its own record is doing the work has not demonstrated that the record is necessary.

ABLATION — which captured field is load-bearing

removed                        class still available
(none — full record)           C2
execution.pure                 none
predicate.id                   C0
policy.resolved_value          C0
candidates.completeness        C1
policy.resolution.revision     C2
read binding (unbound read)    none

The fifth row is not flattering, and it is reported anyway. Deleting the revision from the record costs nothing — the underlying verifier has no concept of a DataHub aspect version, so a record carrying a revision and one missing it certify identically. The revision in the record is documentation for whoever reads it later. It is not evidence, and it does not defend itself.

What is load-bearing is the last row: a read that could not be tied to a revision collapses the class to none. The contribution here is the refusal — declining to certify a decision whose world cannot be named — not the annotation. Both results are pinned by tests.

A record, in full

{
  "action": {
    "id": "drop_column",
    "params_digest": "sha256:58095b73555a42fb25a5cbf38433230df8fb76be6801dcf96bb54a609f3f37d2"
  },
  "candidates": {
    "completeness": "exhaustive",
    "items": [
      {
        "action_id": "drop_column",
        "compared_value": 0,
        "outcome": "admit",
        "predicate_id": "p:61706d6af0ed2414"
      },
      {
        "action_id": "deprecate_instead",
        "compared_value": 0,
        "outcome": "reject",
        "predicate_id": "p:61706d6af0ed2414"
      }
    ]
  },
  "capture": {
    "emitter": "dhdr/0.1.0",
    "sdk_version": "0.1.1"
  },
  "compared": {
    "type": "int",
    "value": 0
  },
  "decision_id": "d-6a1094342393",
  "execution": {
    "clock": null,
    "deps_digest": "sha256:ffc12a86aaf4dee008f1ed48594ceb5aaf17fe51e99379b8dc40ee78736e843b",
    "path_digest": "sha256:b40f716c6efb58b49797f8056cdf40c59220d7105a01787f0df8dfb08ec5bf17",
    "pure": true,
    "runtime": "python3.12.3",
    "seed": null
  },
  "outcome": "admit",
  "policy": {
    "key": "max_safe_consumers",
    "resolution": {
      "provenance": "bundled",
      "revision": "935",
      "source": "datahub:urn:li:dataset:(urn:li:dataPlatform:snowflake,b2fd91.order_entry_db.analytics.order_history,PROD)#upstreamlineage"
    },
    "resolved_value": 0
  },
  "predicate": {
    "expression": "downstream_consumers lte max_safe_consumers",
    "id": "p:61706d6af0ed2414",
    "operator": "lte"
  },
  "rcdr_version": "0.1",
  "reads": [
    {
      "key": "downstream_consumers",
      "source": "datahub:urn:li:dataset:(urn:li:dataPlatform:snowflake,b2fd91.order_entry_db.analytics.order_history,PROD)#upstreamlineage@v935",
      "value_digest": "sha256:5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9"
    }
  ],
  "run_id": "example-then",
  "sequence": 0,
  "ts": "2026-08-06T14:25:52.811192+00:00",
  "writes": []
}

Filed upstream

Two findings from this build went back to the projects they came from:


Verified against DataHub Core v1.5.0.6, mcp-server-datahub 0.6.0, acryl-datahub 1.6.0.17. Every block on this page is a real artifact produced by scripts/generate_examples.py, not a mock-up. Apache-2.0.