DEV Community

GianPiero Bresolin for Indicium AI

Posted on AI-assisted

An understanding of my world: a knowledge graph over my Obsidian notes

I have too many notes

In my job I take a lot of notes. I mean a lot of notes. I attend a lot of meetings, and client conversations, catch-ups with colleagues, workshops and the research I pull together for a pursuit all get written down. My vault holds more than 900 markdown files now, organized into a structure of my own.

The problem is what happens as that folder grows. There are too many notes to consume. The structure still makes sense, but it can't tell me what I actually want to know, which is usually something about people, clients and the commitments between them.

Take a question like "who owns the Northwind data contract?" The answer is in there, in a sentence about Dana Whitfield in a quarterly review note. A search will find the note, along with every other note that mentions Northwind. It won't give me the owner.

My vision

So I wanted a way of managing and storing that data that lets me query it in the right way. I want to ask a question in my own words and get the answer, along with the note it came from. That means storing my notes in a way that understands the people, clients and relationships in them.

My solution

Building it turned out to be the manageable part. In my experience the real problem is maintaining the data over time, and that problem shapes every choice in the rest of this post. The system is a consistent folder structure, markdown notes with OKF metadata, a Graphiti graph with my own ontology, a daily Cowork task that keeps it current, and an MCP server that puts it into my daily work.

I picked a structure and stuck to it

I'll show you mine as an example.

My vault has a "Clients" folder, with a folder for each client. Each one holds that client's information and its own people folder, so I can see the people who belong to that client. There's a top-level People folder as well, and folders that describe our go-to-market work and specific teams. Roughly, it looks like this (Northwind is an invented client):

Obsidian Vault/
  Clients/
    northwind/
      Northwind.md
      2026-09-14 quarterly review.md
      people/
        Dana Whitfield/
          Dana Whitfield.md
  People/
  Blog Posts/
  Strategy/
Enter fullscreen mode Exit fullscreen mode

You're going to query this knowledge store in your own language. When you talk to Claude, you'll use your own references and the aliases you use for people and clients, so the structure has to make sense to you, and mine won't necessarily.

Whatever you choose, follow it consistently, because that consistency is what the rest of this system depends on.

I built on markdown

Markdown is the foundation of all of this.

People who work with LLMs regularly are comfortable with it already. For anyone who isn't, it's a very simple plain-text format, and it's one LLMs really like to work with. Any tool that reads text can read it, so the notes aren't tied to one app.

I write and read mine in an Obsidian vault. It's a good place to do that, and the wikilinks it uses to connect notes come back later in this story.

OKF FTW: I gave every note metadata

So now I have all these markdown files, which is great. What I don't have is a real sense of the state of each one. Nothing has validated it, and there isn't enough metadata around it to build up a graph from.

That's where the Open Knowledge Format comes in. OKF is published by Google Cloud (spec), and I went with it because it's lightweight. All it asks for is frontmatter: a set of keys and values at the top of the note. It's also extensible, so I can layer on new keys and values over time as I work out what I need, and that has been really helpful. Beyond that, it has a lot going for it because it comes from Google, which has the influence to help something like this gain popularity. And I like the idea of frontmatter as a metadata store. It's open-ended enough that it doesn't get in the way of how I already write.

---
type: Meeting Notes
title: "Northwind quarterly review"
description: "Migration scope, timeline pressure, and who owns the data contract."
tags: [meeting, northwind, migration]
generated: { by: claude/haiku-4-5, at: 2026-09-14T11:10:37Z }
---
Enter fullscreen mode Exit fullscreen mode

Now each note carries metadata about itself: what kind of thing it is, a title, a one-line description, some tags, and a record of what produced it. That metadata could really be anything that makes sense of the document, and you can add whatever properties make sense for your organization.

Say you want to mark when a document should be considered out of date. Add a key like expires_after and put a date there. An LLM can read that key and change how it treats the note, as long as you tell it what the key means, so the metadata isn't arbitrary. A deterministic script can act on the same key, and so can anything else that reads frontmatter.

That's what OKF gives me: a lightweight structure I can tie other actions to. A markdown file can feel very unstructured, and OKF brings structure to it without making me rewrite the document to follow some convention. The document can be whatever it wants to be. The metadata carries the information that programmatic solutions need, whether that's an LLM taking a particular action or a script that runs when it sees a particular value. It opens up a whole range of possibilities for how I organize my data.

OKF's own design goals fit this well. Google's argument for it is that the context agents need should live in an open format of its own, somewhere other than a proprietary service or a pile of unstructured text. It's also deliberately minimally opinionated. type is the only field that's always required, every other field is opt-in, and custom keys are preserved. That last part is what makes the expiry example work. (OKF v0.2 does define a stale_after field for exactly that case, but the same holds for any key you invent.)

Frontmatter also changes how much of a note anything has to read. A script can pull one property, such as the type or a date, without touching the body, and the same holds for an agent. Most of the time an agent only needs to decide whether a note is relevant, and the title, description and tags are enough for that.

An agent won't stop at the frontmatter on its own, though. Left alone, it usually reads the whole file. The simplest fix is an instruction in its standing context: check the frontmatter first, and open the body only for the notes that look relevant. A skill or a tool goes further, because it can parse the frontmatter and hand the agent only those properties, so the body never reaches it unless it asks. In my vault, the sweep also rebuilds an index for each top-level folder, with one line per note built from its title and description, so an agent can scan a folder's index before it opens anything.

How I create the metadata

Metadata only helps if every note has it, so keeping the vault in shape is a job for a script. Mine runs as a sweep, in a fixed order. It waits for the vault to go quiet, checks each note, fills in missing metadata, repairs what it can mechanically, links the names it already knows, rebuilds the indexes, scans for secrets and commits. The one step that needs a model is classification, where a model reads a new note and writes its type, title, description and tags. Everything else is plain code, and I'd keep it that way, because a deterministic job like string comparison belongs somewhere that gives the same answer every time.

Keep the list of types small, too. My approved list has about thirty values, and the classifier only chooses from eight of them. Give a model an open list and it invents a plausible new type for any note that doesn't fit, and no query knows that type exists. A note that doesn't fit gets flagged for me to look at instead.

My notes are about relationships

My notes have semantic structure, but mostly they're about relationships: between the notes themselves, and between the things they describe. Obsidian is built on that idea. It uses wikilinks to link documents together, which is how you build up your own knowledge graph inside Obsidian.

Obsidian graph view of my vault: a dense cluster of linked notes, with a few unconnected groups at the edges.

The Obsidian graph view does more than look nice. It shows the relationships between ideas and people, and it also shows the ideas that sit on their own. In the screenshot above, the small dots out towards the edges are parts of my knowledge that haven't been connected with the rest of the graph yet.

It also shows where my attention goes. A dot grows as more notes link to it, so the people, entities and themes I talk about most stand out, and I can see who is really at the center of my world. That's useful for spotting gaps. An idea out on the edge may not be getting enough attention. And if there's someone who should be more prominent in my work and I can't see them in the graph, that's probably telling me there's an action I should take.

The graph serves more than one purpose, and that's one of them. What I also needed was to query my data and pull out relationships, because for consultancy work that's where the value is. Everything I deal with has relationships to everything else: companies, clients, people, organizations, protocols, standards. In a graph, the kinds of relationships are commonly described with an ontology.

I use an ontology to bring meaning

An ontology brings semantic understanding to your data. It says what kinds of things exist and how they can relate, so "Dana Whitfield is accountable for the Northwind data contract" means something a program can work with.

So what I'm doing, ever so slowly, is building up a personalized knowledge domain. It comes in layers:

flowchart TB
    NOTES["Raw notes<br/>the content"]
    OKF["OKF frontmatter<br/>metadata and processing information"]
    ONT["Ontology and graph<br/>entities and the relationships between them"]
    LLM["An LLM asking questions<br/>of my notes"]

    NOTES --> OKF --> ONT --> LLM

The raw notes are the content. The OKF keys are metadata, plus whatever post-processing I want to record. Once I ingest those layers, the things they describe show up as entities, and the relationships between them as typed edges.

This is where it becomes valuable. When the ontology is exposed to an LLM, the LLM can take my question, look at the entities and relationships it knows about, and work out where the relevant information is.

How a note becomes a fact

Here's what that looks like for the Northwind question. From the quarterly review note, extraction pulls out two entities and the relation between them (illustrative):

Dana Whitfield (Person)  --ACCOUNTABLE_FOR-->  Northwind data contract (Contract)
  fact:   "Dana Whitfield owns the Northwind data contract and signs off changes to it."
  source: Clients/northwind/2026-09-14 quarterly review.md
Enter fullscreen mode Exit fullscreen mode

The relation carries a plain-language fact sentence and the path of the note it came from, so every answer the graph gives can point back to its source.

This is also where the consistency from earlier pays off. An entity's identity comes from its type and its name, with no fuzzy matching. Two notes that both say Northwind merge into one node, with no model call. A note that says Northwind Trading instead creates a second node, and it stays separate until someone maps one name to the other. So the names you use consistently in your notes become the identity of things in your graph. A wikilink like [[Dana Whitfield]] helps here too, because it gives extraction the exact spelling to use.

Why I chose Graphiti

I went with Graphiti for the graph because it's a widely used framework with a lot going for it. It lets me define my own entity and edge types in Python, and it brings the schema, typed edges and hybrid search with it.

The bigger reason was speed. Building out a graph, and the database technology underneath it, can take a long time. Graphiti let me iterate quickly and get to the point where I could focus on the parts I cared about: the ontology, the data structure and the taxonomy I wanted to define.

Graphiti needs a database underneath it, and I run FalkorDB in a single local container. It comes with a browser for looking at the graph, which is handy when you want to see what extraction actually produced. For embeddings, which the hybrid search depends on, I use a local model (nomic-embed-text) through an endpoint Graphiti already speaks. The one model the graph pipeline calls out to is Claude Haiku 4.5, and that's there for cost: it's cheap enough to run over every changed note, every day.

How I keep the graph alive

Once the ontology and taxonomy are in place, and there's somewhere to store them, the work isn't finished. My data keeps changing, so I need a daily process that brings in the new notes and notices when they describe a relationship the ontology doesn't have a name for yet.

That's where Claude Cowork comes in.

A scheduled task runs each day in Cowork and manages my data for me. It writes the OKF metadata for new notes, looks at the data, builds up a sense of the relationships in it, and proposes new relation types where nothing existing fits. It runs at 17:00 and only sends what changed since the last run to a model, so a quiet day sends nothing to a model.

Getting those names right matters. The ontology is what brings sense to the data, and the name of a relationship is how the LLM understands what that relationship means. Say Dana Whitfield reports to the head of data at Northwind, but anything that goes wrong on the contract gets escalated to someone else entirely. Those are different relationships. If the graph gives them the same name, then when I ask who's accountable for the contract, the LLM can't tell them apart, and it may well hand me the wrong person.

So a new relation name has to earn its place. Once the same name turns up in three different notes, it's promoted into the ontology automatically, and Claude Cowork can discover it.

The same task can keep the graph clean as well as growing it, and this is where the OKF metadata from the beginning comes back in. Notes go out of date. Remember the expires_after key? A scheduled task can read that date and treat an expired note differently, retire its facts from the graph, or flag it for me to review. The idea works for any property you define. Your metadata can decide what gets cleaned up, what gets removed from the graph because it's no longer relevant, and how the rest of the data is organized. That interplay between a lightweight metadata format and a task that runs every day is where a lot of the possibilities open up.

I think this applies well beyond ontologies. Any document that tells a model how things currently stand will go out of date, and nothing in the document will tell the model so. That's the kind of problem OKF metadata like an expiry date is there to solve.

Where I use it every day

The last piece is an MCP server that exposes the knowledge graph. Because it's MCP, the graph isn't only available to Cowork. Any agent that can access a local MCP server can use it. I use it with Cowork, and that's where all the pieces, and all the work that came before, start to tie together.

This is where the value actually comes alive. Without this step, it's fancy metadata on my notes. The wikilinks add something too, because they let me see my graph in Obsidian. But the real value, in my mind, is being able to integrate this into my daily work. In Cowork I can ask questions about my own notes and get answers that come from an understanding of my world, and connecting the graph through MCP opens up a whole host of possibilities beyond that.

My MCP server

The server itself is small: one Python process that talks over stdio. It needs no API key, because search runs on the local embeddings and queries go straight to FalkorDB. It gives Claude seven read-only tools, and two of them show the idea well. entity_profile returns a summary of what the graph knows about a named entity. find_relations helps Claude work out which relationship fits a question when it doesn't know the relation name, and that's how the ontology becomes something Claude can discover.

The server also carries instructions, which Claude reads when it connects. Mine tells it to check the graph before a Slack or web search when I ask about my own notes, and to name both sources when Slack and the graph disagree. That matters, because the graph only knows what my notes captured. If Claude filled a gap from the web, the answer would read as though it came from my notes, and I would repeat it with the same confidence.

What leaves my laptop

Much of my work is in regulated environments, so I'd call this out. The graph, the database and the embeddings all stay on my laptop, but every model step in the graph pipeline sends note text to Claude Haiku 4.5, the scheduled task itself runs in Cowork, and answers go to the Claude app that asked for them. Make sure that's acceptable for what's in your notes before you point this at them. The server only runs on my laptop, so a web or phone session can use it only through the Claude desktop app on my laptop, while that app is running.

Here's what Claude gets back when it profiles the contract (illustrative):

Northwind data contract (Contract)
  ACCOUNTABLE_FOR  <- Dana Whitfield
    "Dana Whitfield owns the Northwind data contract and signs off changes to it."
    source: obsidian://open?vault=Obsidian%20Vault&file=Clients/northwind/2026-09-14%20quarterly%20review
Enter fullscreen mode Exit fullscreen mode

So, who owns the Northwind data contract? Dana Whitfield, according to the quarterly review on 14 September, and the answer comes back with a link to the note.

What I'm building next

I've also done a test run of Qwen 3 locally on Apple's MLX framework, using the 14B model quantized to 4-bit. The next step is to see whether a local model like that can take over the model steps. Today every model step sends note text to Claude Haiku 4.5. A local model would keep that text on the laptop, which matters for anyone whose notes come from regulated work.

Top comments (0)