Inspiration

An agent asked our warehouse for a table that wasn't there.

Postgres logged it. Nobody read the log. The agent moved on and forgot.

That happens hundreds of times a week in any company running agents, and none of it is written down anywhere. It used to be, because the people who couldn't find a table filed a ticket. Agents don't file tickets. They fail quietly, in their own context, and the failure is deleted.

So the wish list stopped being written down at exactly the moment it started growing fastest.

A catalog is an inventory. It answers what do we have. It has no way to answer what did we not have, that somebody wanted, because the wanting happens outside it. The search returns nothing and that's the end of it.

We made the miss the record instead.

What it does

An agent searches DataHub, misses, and the miss becomes a real dataset URN tagged ghost. It carries a demand counter and an edge back to every agent that asked.

Three agents in three separate processes ask for the same thing. None of them knows about the other two. They converge on one entry.

At threshold a builder agent reads the backlog. The first thing it does is refuse. Fifty one tables, and for each one it can't build it prints the missing column. No column for cohort. No column for churn rate.

Then it finds the one this warehouse can actually answer, reads the three agents' registered queries to work out the grain, discovers the source table through DataHub, validates the SQL against the live warehouse, and opens a pull request.

A human merges it. The ghost goes solid. Real schema, real lineage, and the three agents become Owners of the table they caused to exist.

And then the part I hadn't seen anyone do. The three queries that couldn't run, run. Same SQL, real rows. None of those agents asked again.

No adoption needed either. Postgres has been logging relation does not exist for twenty years with the query underneath it. We read that log. 2,405 real errors became 1,253 attributed requests across 51 tables that don't exist.

How we built it

The requesters and the builder are real MCP clients. Separate processes, identity taken from the clientInfo handshake, so the demand on a ghost can't be typed in.

Ghosts are DataHub datasets carrying structured properties, Queries, ownership, schema and upstreamLineage. There's a demand ingestion source too, so a stock datahub ingest recipe can load it.

Two agents built it over one weekend, in parallel, with a file ownership contract between them. A Cursor cloud agent on the plumbing. Claude Code on the agent layer and everything a judge touches.

Challenges we ran into

A run that looked healthy and wasn't. structuredProperties writes were rejected 422 for hours while the local store kept advancing. The board kept rendering old ghosts. Nothing new reached the catalog. No test caught it. I found it by asking whether a fresh want could be created at all.

Solid meant a tag, not a table. dbt had never actually run. information_schema returned zero rows for a model the builder had just shipped. So we changed what done means: not DataHub returned a tag, but the requester's query returns rows.

Our own verification command was destructive. eval --cold called nullspace reset, which hard deletes every asset. It destroyed the demo data twice. Once while I was reviewing our own work.

And receipts from one machine are not receipts. Night run results were true in a cloud sandbox and false on the laptop that records the demo.

Accomplishments that we're proud of

A merged pull request an agent wrote and opened by itself, with a body that argues its own grain from three agents' registered queries.

The builder refusing. Stepping over fifty one wants, naming the missing column for each, before claiming the one it can do.

2,405 genuine Postgres errors harvested into a ranked backlog, with no change to any of the services that produced them.

scripts/without-datahub.sh. Kill DataHub and three agents fail in three silos, zero ghosts get written, and the board reports the catalog unreachable and shows nothing. It exits non zero if any of that silently succeeds.

What we learned

Metadata can't settle every claim.

These agents are no longer blocked is only provable by running their queries. So that one number on our board is a live READ ONLY execution, not a read back, and it's labelled as the exception.

The harder one: a witness has to name the machine it was witnessed on. Almost every real defect this weekend was hiding behind a receipt that was true somewhere else.

What's next for Nullspace

Snowflake query history. dbt failures. BI errors. Postgres logs are one source and the easiest one.

Letting anyone claim a ghost, not just our builder.

And the one worth doing: demand aggregated across companies. A table wanted at four hundred of them is a schema standard forming in public.

The mechanic is a demand counter and a threshold. DataHub could build it in a weekend and if this is right they should, which is why the RFC is upstream rather than kept. What you can't copy is the record of what your agents needed and couldn't get, and that gets longer every day.

Built With

  • claude
  • cloudflare-tunnel
  • cursor
  • datahub
  • dbt
  • docker
  • docker-compose
  • fastapi
  • git
  • github-api
  • graphql
  • kafka
  • mcp
  • model-context-protocol
  • opensearch
  • playwright
  • postgresql
  • pytest
  • python
  • vercel
Share this project:

Updates