← The ADLC library
Open knowledge · 5

Markdown links as a knowledge graph

A directory of linked markdown files is a graph nobody had to build. It is weaker than a real graph database in ways that matter, and stronger in the one way that matters most.

A colleague once described a knowledge graph project as “eighteen months of ontology, followed by a redundancy round.” That is unkind and not entirely fair, but everyone who has been near one of these efforts recognises the shape. The modelling is genuinely interesting. The population is genuinely hard. And the moment of truth, when somebody asks the graph a question that matters and gets an answer that is subtly wrong because an edge was never created, tends to arrive after the budget has been spent.

The Open Knowledge Format’s answer to relationships is almost insultingly simple. Concepts link to each other with ordinary markdown links. [the events table](../tables/events.md). That is it. The directory becomes a graph because files point at other files, and because the file path is the concept’s identity, a link is unambiguously an edge between two concepts.

No edge types. No cardinality. No inverse relationships. No query language. Just links, in prose, written by whoever was writing the sentence.

I want to argue that this is much better than it sounds, and then be specific about where it is genuinely worse.

Why this works at all

The reason a pile of markdown links functions as a usable graph comes down to who is reading it.

A traditional knowledge graph is built for a query engine. The engine cannot read, so every fact must be expressed as a typed edge between typed nodes, because typed edges are the only thing it can traverse meaningfully. The modelling effort exists to compensate for the reader’s inability to understand language.

That constraint is gone. The reader now understands language. When a document says “churn is computed from the subscription events table, excluding trial accounts,” a model reading that gets both the edge and its meaning from the same sentence. The relationship type is expressed in the prose around the link, which is where relationship types were always most naturally expressed, and where they are richest: “excluding,” “derived from,” “supersedes,” “is the fallback for,” “was rejected in favour of.” A typed-edge schema would have to enumerate those in advance and would inevitably miss most of them.

So the format is not skipping relationship modelling out of laziness. It is putting the semantics in the medium the reader is best at, and using the link only for the part the reader is bad at: knowing precisely which document is meant.

Modelled graph

  • Edge types decided up front
  • Population is a separate project
  • Nuance flattened into the nearest type
  • Reader is a query engine
  • Answers are precise and often incomplete

Links in prose

  • Relationship stated in the sentence
  • Population is a side effect of writing
  • Nuance survives, because it is language
  • Reader understands language
  • Answers are contextual and unverifiable
Both columns have a real cost. The right-hand one is cheap to populate and expensive to trust; the left-hand one is the reverse.

What the graph is actually for

It helps to be concrete about what an agent does with these edges, because “knowledge graph” invites people to imagine analytics dashboards, and that is not the use.

Neighbourhood loading. The agent has a task about the churn export. It finds metrics/churn-rate.md, reads it, and follows the two or three links that document contains. It now has the metric, the source table and the definition of a trial account. That is a coherent, complete, small context assembled by traversal rather than by similarity. This is the single most valuable behaviour the graph enables and it needs nothing beyond one hop.

Blast radius. Before changing the definition of a trial account, the agent can find every document that links to it. That is a backlink query, which requires nothing more than grep across the directory. It answers “what else believes something about this” without anyone having maintained a dependency list, because the dependency list is the set of inbound links.

Orientation. A new agent (or a new engineer) landing in an unfamiliar area can walk from a service to the tables it owns to the metrics computed from those tables. Not because someone drew a map, but because whoever wrote each document linked to the things they mentioned.

EntryThe task names a concept. Find its file.
Read wholeOne concept, one file, loaded complete.
One hopFollow the links in that document.
BacklinksGrep for who points here. That is the blast radius.
Four operations, none of which need a query language or an index server.

The backlink one deserves emphasis because it inverts a familiar failure. In most systems, the question “what depends on this?” is answered by a document somebody maintains, which is to say it is answered wrongly. Here it is derived from the corpus itself. It cannot be more current than the corpus, but it also cannot be less current, and that property (derived rather than declared) is the same property that makes source control the only trustworthy record of what a team has built.

Now the uncomfortable part. In every markdown knowledge base I have looked at, the links are the least accurate thing in it.

The reason is mechanical. Markdown links fail silently. There is no compiler, no foreign key, no runtime error. A link to a file that was renamed six months ago renders as text and resolves to nothing. A human clicking it gets a 404 and shrugs. An agent following it gets nothing and proceeds without the context it was supposed to have, and nothing anywhere records that a lookup failed.

That is the worst kind of failure: silent, invisible, and degrading the quality of output in a way that looks like the model being a bit off rather than the knowledge base being broken.

Which leads to the single highest-value piece of automation for anybody doing this, and it is embarrassingly cheap.

knowledge/metrics/churn-rate.md:14
  -> ../tables/subscription-events.md   OK
  -> ../concepts/trial-account.md       MISSING
  -> ../metrics/mrr.md                  OK

1 broken reference in 214 documents

A link checker over the knowledge directory, run in CI, on every pull request. No models involved, no infrastructure, and it catches the most common way a knowledge base comes apart while every individual file still looks fine.

Run that from day one. Not because broken links are the biggest risk, but because they are the only risk in this whole area that is trivially detectable, and you should always take the free wins before the hard ones.

The graph you cannot see

There is a second class of decay that no link checker finds, and it is the more serious one: relationships that exist in reality and were never written as links.

A new service starts consuming the subscription events table. Nobody updates tables/subscription-events.md to mention it. The edge exists in the world and not in the graph. Every backlink query from now on returns an incomplete answer, and returns it confidently, which is the problem, because an incomplete answer that presents as complete is how you ship a breaking change.

The traditional answer is discipline, and discipline does not scale. The interesting answer, and the one this whole publication keeps circling, is derivation: relationships that can be read from ground truth should be read from ground truth rather than written by hand. A dependency between a service and a table is, in most codebases, visible in the code. A knowledge base that hand-maintains facts which are mechanically derivable from source control has chosen to maintain a copy, and copies drift.

So the discipline I would suggest is: hand-write the edges that encode meaning (this metric excludes that category, this policy supersedes that one, we rejected this approach for that reason), and derive the edges that encode structure (this service reads that table, this job writes that dataset) from the artefacts that already contain them. The first kind is knowledge. The second kind is inventory, and inventory maintained by humans is inventory that is wrong.

Where this breaks down

You cannot query it. I said in an earlier piece that this is a real loss, and it is worth being concrete. “List every metric that ultimately derives from a table containing personal data” is a two-line query in a graph database and a scripting exercise here, and the script will be approximate because it has to interpret prose to know what “derives from” meant in each case. If your primary use for the graph is analysis rather than context loading, this format is the wrong tool and you should use a real one.

Untyped edges mean traversal cannot be trusted mechanically. A link from a decision record to a rejected approach and a link from a metric to its source table are the same object in the file. Any tool that walks the graph without reading the prose will treat them identically, and produce results like “this metric depends on the queue we decided not to use.” Which means automated traversal beyond one hop is unreliable, and multi-hop reasoning has to be done by something that reads, which puts you back in context-window economics.

Silent failure is the default and always will be. A link checker fixes broken paths. Nothing fixes links that point at the wrong document, or prose that describes a relationship that no longer holds while the link remains valid. The graph can be fully connected and substantially false.

Missing edges are invisible and the format offers nothing here. This is the biggest structural weakness. There is no way to distinguish “this concept has no dependencies” from “nobody wrote them down.” Backlink queries therefore have the property that their negative answers are worthless, and negative answers are exactly what people use blast-radius checks for.

It is v0.1, and the relationship story is the part most likely to change. If the spec later introduces typed relationships or a manifest of some kind, prose-embedded links will need supplementing. I would not build tooling that assumes today’s model is final. Writing ordinary links costs nothing and survives whatever comes next, which is the argument for doing it now anyway.

And every link is another thing to maintain. A densely linked knowledge base is more useful and more expensive. Sparse linking degrades gracefully into a pile of documents. Dense linking degrades into a graph full of confident lies. Pick your density deliberately, and link most heavily where the relationships are most stable.

The takeaway

Markdown links give you a knowledge graph as a side effect of writing sentences, which is the only way anybody has ever successfully populated one. The edges are untyped and the semantics live in the surrounding prose, which is fine when your reader understands prose and useless when your reader is a query engine.

Use it for what it is good at: loading a coherent neighbourhood of context, and answering “what else believes something about this” from backlinks rather than from a hand-maintained list. Do not use it as an analytics substrate. Check the links in CI from the first week, because that is a free win. And hand-write the edges that carry meaning while deriving the ones that are mere inventory, because inventory maintained by people is inventory that is out of date.

The next piece moves from structure to a specific payload: what happens when acceptance criteria stop being a field in a ticket and start being portable, linkable knowledge that outlives the sprint that produced it.