A customer success manager finishes a call with a strategic account. The recorder does its job. Within ninety seconds the transcript is written to the CRM, logged against the account, summarised into a note, and two follow-up tasks are created. By every measure of integration health, this worked. The systems talked. Nothing was dropped.
Eleven minutes into that call, the customer said this:
“We're not signing the renewal until case #48213 is closed. It's been open six weeks and it's the reason procurement has gone quiet on us.”
That sentence is the most important thing anyone said all quarter. It establishes a hard dependency between a support case, a renewal opportunity, a contract date, and the reason a champion stopped replying to email. It is the answer to the question your CRO will ask on the forecast call in three weeks.
Now go and find it in a system.
The case exists in Zendesk, and it's real, and someone is working it. The opportunity exists in Salesforce with a close date. The call exists, attached to the account. Three real records, all correctly stored, all correctly integrated — and the relationship between them exists nowhere at all.
The recorder didn't create it, because “call blocks renewal via case” is not an edge its data model knows about. The Zendesk–Salesforce integration didn't create it, because that integration syncs cases to accounts, not cases to opportunities-at-risk. No human created it, because creating it would mean opening three tabs and updating a field nobody reads.
So the relationship stays where it was born: in a sentence, in a transcript, in a system that treats that transcript as an attachment.
This is a dark join — a relationship that is real, load-bearing and actionable in your business, and which exists in no schema anywhere in your estate. The relationship isn't in the data. It's in the work. Once you start looking for them, you find that your business runs on them almost entirely.
Your join keys are in the conversation
Here is the fastest way to make this concrete. Take the question your executive team actually asks — which renewals are blocked by open support issues? — and try to write it.
SELECT o.name, o.close_date, c.case_number, c.status
FROM opportunities o
JOIN calls ON o.account_id = calls.account_id -- fine
JOIN cases c ON ??? -- there is no key
WHERE o.close_date < CURRENT_DATE + 90
There is no key. There has never been a key. Not because someone forgot to model it, and not because your integration is misconfigured, but because the fact that this case blocks this renewal was never data. It was work. It happened in the eleventh minute of a Tuesday call, and it was restated four more times that week — in a follow-up email, in a Slack thread, in the comments on a shared doc, and in a forward to the platform team. Five declarations of the same relationship, all of which entered your estate as unindexed strings inside text blobs, hanging off records that couldn't represent them.
You cannot join on it. You cannot ETL it. You cannot MDM your way to it. You can pay a systems integrator for eighteen months and at the end of those eighteen months the join key will still not exist, because integration projects move data that exists — and this relationship never existed as data in the first place.
Which is worth stating flatly, because everything else follows from it:
A relationship that was never recorded cannot be derived from the records. The relationship isn't in the data. It's in the work.
Why we have been looking in the wrong place
The assumption underneath enterprise data practice — through EAI, through MDM, through the data lake, through the modern warehouse, and now through the enterprise knowledge graph — is that relationships live in the schema. Find them by resolving entities. Reconcile the identifiers. Build the golden record. When a relationship is missing, the diagnosis is always the same: the data isn't clean enough yet, so fund another phase.
For a certain class of relationship this is correct and it works. Is ACME Corp the same customer as Acme Corporation Ltd? is a schema question with a schema answer, and MDM is genuinely good at it.
But look at what your organisation is actually trying to find out. Which renewals are blocked by unresolved engineering work? Which accounts referenced the competitor we just lost two deals to? Which of the twelve tickets this customer raised are the same underlying problem? Which roadmap commitments were made verbally in QBRs and never entered anywhere? Why did this account go quiet?
Not one of these is an entity resolution problem. Every one of them is a relationship that a human declared plainly, in the course of doing their job, and then never typed into a system. MDM is an extremely expensive answer to a question nobody was asking.
The deeper issue is one of direction. Every tool built to address this works the same way round: take the human artifact and attach it to the record. The recorder attaches the call to the account. The email plugin attaches the thread to the contact. The support integration attaches the ticket to the case. Each is a one-way association from unstructured content into the one structured record the tool happened to know about.
And the moment you write it out like that, you can see the mistake.
The call is not about the account. The call is about a renewal, a support case, a competitor, a missed commitment, a person who is leaving, and a contract clause. The account is simply the only one of those the recorder was capable of representing. Every other relationship in that call was discarded at ingest — not because it was unimportant, but because there was no field for it. The same is true of the email, the thread and the doc: each carried a dozen relationships in, and each had exactly one place to put them.
So here is the inversion:
It is not that the work belongs to the record. It is that the work tells you which records belong together.
Your database is not a map of your business. It is a log of it — an artifact of work already done, recording what someone remembered to enter. The structure of the work lives in the action layer. That isn't the messy exhaust of the real system; it is where the relationships are declared. The database is the exhaust.
The action layer is bigger than the meeting
It would be easy to read the opening story as being about calls and conclude this is a transcription problem. It isn't. Calls are simply the most vivid example. The action layer is the entire human surface of your business — the tools your people use to do the work, as opposed to the systems they are required to log the work into: meetings and their transcripts, email, Slack and Teams, shared documents, the comment threads on those documents, and the bodies of tickets as distinct from their fields.
There is a reason this layer is so rich, and it is the whole mechanism.
Your people treat these as productivity tools, not databases. Nobody thinks of a Slack message as a data entry exercise. There is no required field, no validation rule, no admin asking why the picklist is blank, and no sense that describing a situation properly is bureaucratic overhead. So when someone needs to convey that this case is blocking that renewal, they simply say it — accurately, with identifiers, and without resistance.
Whereas in the CRM, the same person faces a form. The form has no field for “blocks,” no field for “same root cause as,” and no field for “champion went quiet because of this.” So the relationship is either mangled into a free-text note or, far more often, not entered at all. The schema is a filter, and it filters out precisely the relationships that don't fit a shape someone anticipated years ago.
This is the quiet asymmetry at the heart of the problem: the systems designed to capture structure are the ones least able to capture the structure that matters, and the systems nobody considers to be systems of record are where the real structure gets declared. People are not failing to document their work. They are documenting it constantly and diligently — in email, in threads, in docs — just not into the place you built for it.
And they do it with more precision than they get credit for, because that is how coordination works. “Blocked by NOD-231.” “Duplicate of the ticket Sarah raised last week.” “Same root cause as the Contoso deployment.” “Per section 4 of the MSA.” A meeting agenda doc listing seven ticket numbers, two roadmap items and the contract — a dense little graph, hand-built by someone who understood the account, sitting in a folder, relating to nothing.
Why this is suddenly urgent
Dark joins have existed for as long as enterprises have had both databases and meetings. What changed is that you are now being asked to deploy agents.
Every AI initiative currently stalled in your organisation is stalled for the same reason, and it is almost never the model. Ask an agent “what's the risk on the Contoso renewal?” and it will do exactly what it was built to do: retrieve the account record, retrieve some documents that mention Contoso, and produce a fluent summary of the fields you already had. It will not tell you the renewal is blocked by a six-week-old case, because nothing in its context says those two things are related.
RAG retrieves documents. It does not know which records a document is about. No amount of prompt engineering, context window or model upgrade supplies that, because the information was discarded at ingest — long before any model was involved. And it compounds: every new integration you add contributes more nodes and zero new edges. You can integrate forever and the graph stays sparse.
The corollary is the good news. This is a data-layer problem sitting underneath an agent problem — which means you fix it once, and every agent you deploy afterwards inherits the fix.
Measure your own dark join rate
Don't take any of this on assertion. There is a number you can put on it, for one account, in an afternoon.
Take a sample of the human artifacts attached to a single important account — calls, email threads, chat conversations, shared docs. Count the references pointing at something outside the record each one is filed against: a ticket number, a Jira key, a contract clause, another document, a different customer, a competitor, a roadmap commitment. Then check how many of those relationships exist as an actual, traversable link in a system you own.
Unlinked references ÷ total references is your dark join rate.
The first count typically comes back well north of half. The second typically comes back close to zero. Email and document comments score worst, because no vendor has ever built even the naive one-way integration for them.
That ratio is the honest measure of how much of your operating reality your systems can represent — and a more useful number to take into a budget conversation than another data-quality score, because it measures the thing your executives are complaining about when they say the CRM is useless. A high rate is also normal. Every estate we have seen scores high, including ours; it is a structural gap, not a hygiene failure.
We've published the instrument that does the counting — free, unbranded and CC BY 4.0 — as the Dark Join Diagnostic. It keeps the tally as you read, works out the rate and the split by surface, and builds a ledger of the specific relationships in your business that exist in no system. That ledger, rather than the percentage, is what tends to end the argument. Nothing leaves your browser.
How you find them: ingestion chaining
If you accept the premise, the design follows: you need something that observes the action layer and precipitates a graph from it. Three mechanisms do most of the work — at Noded we call the combination ingestion chaining, and it is the whole answer to “fine, but how do you get the relationship out of the work?”
Interaction rank. Treat every touch as an edge — a forward, a mention, a status change, a reply, even a read. Records the work keeps reaching for climb in rank. This is PageRank applied to enterprise work rather than the web, and it inherits PageRank's central virtue: it doesn't need to be told what's important, because importance is a property of the link structure.
Temporal decay. Importance decays unless renewed. A record nobody has touched for a business cycle falls out of the active chain. This is what stops the graph becoming another lake — most enterprise knowledge systems fail on exactly this axis: they accumulate, they never forget, and within a year the signal is buried under everything that used to be true.
Intent filter. This is the one that determines whether the whole thing is trustworthy, and it's a classifier over the surrounding language rather than the identifier alone — applied the same whether that language was spoken on a call or typed in a thread. “Blocked by NOD-231” chains the record in. “Closing as duplicate of NOD-231” keeps it out. The identifier is identical; the relationship is opposite. Naive extraction — regex for anything shaped like a ticket ID — produces a graph so noisy it is worse than nothing, because a wrong edge in a customer graph costs more than a missing one.
Two things this design deliberately does not do. It does not move your data: the graph holds pointers, not copies, and records stay in the systems that own them under those systems' permissions. And it does not replace your warehouse: aggregate questions belong in BI, and always will. Snowflake and Looker answer what happened across the business. A context graph answers what's happening with this account right now, and why — a per-account, live, relationship-shaped question a star schema was never built to serve.
You could build all of it. If you're going to, build it honestly: connectors across the full action layer including email and doc comments, interaction rank over a continuously-updating edge stream, a decay model tuned to your business cycle, an intent classifier with a labelled evaluation set so you can prove precision rather than assert it, permission-aware resolution so the graph never becomes a lateral disclosure path, and a cached serving layer or your token spend will sink the business case on its own. That is a real platform team and a real multi-quarter programme.
If you'd rather have it running than build it, that is what Noded is. We read the action layer rather than the storage layer, assemble a live context graph per account from pointers rather than copies, and expose it to Claude, ChatGPT, Gemini or your own agents through a single governed MCP layer. Data stays in the systems that own it, write-backs are opt-in and attributed, and nothing is ever trained on. SOC 2 audited.
We don't think a platform decision at this layer gets made from a free trial, so we haven't built the ask around one. The sensible next step is a working session: we walk your architecture, show you what ingestion chaining surfaces on an estate shaped like yours, and take the security and governance questions before anything is connected to anything.
Either way, run the diagnostic first. One account, thirty artifacts, an afternoon. Find out how much of your business your systems can actually see.
Because the relationship isn't in the data. It's in the work — and the work is already written down.