Install
Typed Graph works on desktop and mobile. Pick one:
- Community plugins (recommended): open Typed Graph in Obsidian, or go to Settings → Community plugins → Browse → Typed Graph → Install → Enable. Directory page: community.obsidian.md/plugins/typed-graph.
- BRAT, for beta builds: install Obsidian42 - BRAT, run BRAT: Add a beta plugin and enter
Volland/obsigraph. - Manual: from the latest release copy
main.js,manifest.jsonandstyles.cssinto<vault>/.obsidian/plugins/typed-graph/, reload, enable.
Quick start
- Give a note a type: add
type: Personto its frontmatter. - Write an edge line:
knows:: [[Bob]] {since: 2020}. - Add a query block anywhere:
```graph-query MATCH (a:Person)-[r:knows]->(b) RETURN a, r, b ```
- Open the explorer with Typed Graph: Open graph view or the ribbon icon.
Edges
An edge is one line in a note; the note is the source.
knows:: [[Bob]]
knows:: [[Carol]] {since: 2018, label: "met at conf"}
-distrusts:: [[Eve]]
+trusts:: [[Bob]]
- works_at:: [[Acme]], [[Initech]] {role: engineer}
| Part | Meaning |
|---|---|
type:: | Edge type. Graph Link Types lines work unchanged. |
- / + prefix | Sign: −1 or +1 (default +1). weight is an ordinary property. |
{…} | Properties: numbers, booleans, quoted or bare strings, lists. |
{id: "…"} | Pins the edge id; otherwise it is source#type#target#n. |
| heading | The nearest heading above is recorded with the edge. |
Lines in code blocks and frontmatter are ignored. Malformed property blocks keep the edge and appear in Show diagnostics. Links to missing notes become faded stub nodes.
Typed nodes
Every note is a node. Frontmatter type becomes its labels; a list gives several:
--- type: [Person, Employee] age: 40 ---
Frontmatter fields, title and path are node properties.
Schema notes
A note directly in Types/ (configurable) describes the type named by its title:
---
schema:
properties:
status: {kind: text, default: active}
born: {kind: date, required: true}
edges: [knows, works_at]
visualization:
color: "#7c5cff"
shape: ellipse
icon: user
label: name
edges:
knows: {color: orange, line: dotted}
---
## Notes
- Kinds:
text,number,boolean,date,link. edgeslists allowed outgoing edge types; omit it to allow any.- The body is the template for Create note from type, which never overwrites a note.
- Validation is advisory: issues show in the status bar and Show diagnostics.
Edge property embeds
We met in {{edge: Alice -knows-> Bob . since}}.
{{edge: met-2020 . since}}
{{edge: met-2020}}
Find an edge by endpoints and type or by pinned id; . property shows one value, otherwise a small table. Put a sign before the type to require it: --distrusts-> (negative), -+knows-> (positive). Embeds referencing unpinned edges get a diagnostic suggesting an id.
Query blocks
A graph-query block is an optional header, a blank line, then the query.
```graph-query view: table columns: who, since backend: builtin node.Person: color=#e5484d, shape=hexagon edge.knows: line=dashed MATCH (a:Person)-[r:knows]->(b) RETURN a.title AS who, r.since AS since ```
| Option | Values |
|---|---|
view | auto (graph when the result has nodes, relationships or paths), table, graph |
columns | Table columns to show, in order |
height | Graph height in pixels (100–4000) |
backend | builtin or ladybug (needs the sidecar) |
node.<Type> / edge.<type> | Style overrides for this block |
Results refresh live. Graphs larger than the element limit fall back to a table.
openCypher subset
- Clauses:
MATCH,OPTIONAL MATCH,WHERE,WITH,RETURN [DISTINCT],ORDER BY,SKIP,LIMIT. - Patterns: labels, property maps,
-><--,[:a|b],[:knows*1..3],p = (...). - Aggregates:
count(*),count,sum,avg,min,max,collect(withDISTINCT). - Functions:
id type labels keys properties startNode endNode length nodes relationships toLower toUpper trim replace substring split left right size coalesce toString toInteger toFloat abs round floor ceil sign head last reverse. - Built-ins:
n.stub,n.title,n.path,r.id,r.sign.
CREATE, SET, DELETE, MERGE and REMOVE are rejected. UNWIND, UNION, CALL, CASE and =~ are not in the built-in subset; use backend: ladybug for those.Styling
Every attribute resolves on its own; the most specific source wins:
- Block header (
node.Person: color=red) - Schema note
visualization - Settings → Type styles / Edge styles
- Defaults: a stable color per type; negative edges dashed red with a tee arrow
Node attributes: color, shape (ellipse, rectangle, round-rectangle, diamond, hexagon, triangle, star), icon (Lucide name), label (property shown instead of the title). Edge attributes: color, line (solid, dashed, dotted).
Graph view
- Empty query: follows the active note's neighborhood.
- Enter a query and press Run or ⌘/Ctrl+Enter.
- Right-click or long-press a node to expand it; double-click or ⌘/Ctrl-click to open its note.
- Select a node or edge to see its properties and where each style came from.
This is a separate view. Obsidian's core Graph view is left untouched, because it has no plugin API and patching it breaks on Obsidian updates. It still draws one plain line per linked pair of notes, with no edge types, signs or properties.
Color types in the core Graph view
You can still color nodes by type in the core Graph view, without any plugin code. Open the core Graph view, expand Groups, choose New group, and enter a property search such as [type:Person], then pick a color. Add one group per type. The first matching group wins, so order groups from most to least specific.
The sidecar
An optional headless Node service over a read-only copy of your vault. It powers the Ladybug backend, vector search, GraphRAG and MCP.
npm install && npm run build:sidecar OBSIGRAPH_VAULT=/path/to/vault OBSIGRAPH_DATA=/path/to/data OBSIGRAPH_TOKEN=change-me \ node packages/sidecar/dist/server.mjs
Docker: packages/sidecar/Dockerfile and compose.example.yaml mount the vault :ro, run as non-root and publish on host loopback only.
| Endpoint | Purpose |
|---|---|
GET /health | Liveness (no token) |
GET /status | Graph, mirror and vector state |
POST /query | {query, params, backend} → {columns, rows} |
POST /search | Vector search, optional then Cypher with $hits |
POST /retrieve | GraphRAG retrieve |
POST /vectors/rebuild | Re-embed after a model change |
POST /mcp | MCP over streamable HTTP |
| Variable | Default |
|---|---|
OBSIGRAPH_VAULT, OBSIGRAPH_DATA | /vault, /data |
OBSIGRAPH_TOKEN / OBSIGRAPH_TOKEN_FILE | required (except --stdio) |
OBSIGRAPH_HOST, OBSIGRAPH_PORT | 127.0.0.1, 8765 |
OBSIGRAPH_POLL_MS | 0 (set e.g. 5000 on macOS bind mounts) |
OBSIGRAPH_LADYBUG, OBSIGRAPH_VECTORS | on; set 0 to disable |
OBSIGRAPH_EMBED_PROVIDER, _URL, _MODEL, _KEY/_KEY_FILE | ollama, http://localhost:11434, nomic-embed-text |
Ladybug backend
The sidecar mirrors the graph one-way into LadybugDB. Set the sidecar URL and token in plugin settings, then use backend: ladybug in a block. Queries the built-in parser reads are translated so they return the same results. A 52-query conformance suite keeps the two engines in agreement.
Other read Cypher (UNWIND, CASE, UNION, =~) runs on LadybugDB untranslated, against the mirror layout. The result carries a notice saying so. In that layout:
- Every note is a
Node, withtitle,path,stuband alabelslist. - Each edge type is its own relationship table.
- Properties are typed columns named
p_<name>_<kind>. The kind iss(text),n(number),b(boolean),lsorln(lists) orj(anything else).
```graph-query
backend: ladybug
view: table
MATCH (p:Node)-[c:contributes]->(proj:Node)
WHERE list_contains(p.labels, "Person")
RETURN p.title AS person, proj.title AS project,
CASE WHEN c.p_hours_n >= 100 THEN "core" ELSE "helper" END AS involvement
```
Vector search
Notes are chunked by heading with title, type and frontmatter as context; each edge becomes a sentence like Alice (Person) knows Bob (Person) - met at NeurIPS. Embeddings come from local Ollama by default.
POST /search
{"query": "machine learning", "target": "nodes", "k": 5, "types": ["Person"],
"then": "MATCH (p)-[:works_at]->(c) WHERE id(p) IN $hits RETURN c.title"}
GraphRAG
POST /retrieve
{"question": "Who works with Alice on search?", "k": 5, "depth": 1}
Returns the best chunks of the vector hits and of their graph neighbors (hits first, then by distance), each with path, heading, score and role, plus the connecting edges. Hub expansion is capped and flagged.
MCP for agents
claude mcp add typed-graph \ -e OBSIGRAPH_VAULT=/path/to/vault -e OBSIGRAPH_DATA=/path/to/data \ -- node /path/to/packages/sidecar/dist/server.mjs --stdio
Tools: cypher_query, vector_search, graphrag_retrieve — all read-only. Over HTTP use POST /mcp with the bearer token.
Security
- The vault is never written: mounted read-only, with every write confined to the data directory.
- Bearer token on everything but
/health; loopback bind by default; body and time limits. - Read-only queries enforced twice on Ladybug: a guard and a read-only database snapshot.
- Retrieved note text is marked as untrusted content for agents.
Settings
| Maximum graph elements | Above this, graphs show as tables. |
| Maximum path depth | Cap for unbounded * patterns (default 10). |
| Default query backend, Sidecar URL, Sidecar token | For backend: ladybug. |
| Schema folder, Show diagnostics | Type schemas and the issue counter. |
| Type styles, Edge styles | JSON style defaults. |
| Refresh delay | Debounce before blocks re-run. |
FAQ
Does it change my notes?
No. The plugin and the sidecar only read notes. Only Create note from type and Create schema note create new files, on request.
Do I need Graph Link Types or Dataview?
No. Typed Graph reads the same type:: [[Target]] lines on its own.
Does my vault leave my machine?
Not by default. Embeddings use local Ollama; a hosted endpoint is only used if you configure one.