Use case or problem
links and backlinks answer for a single file. There is no command for the vault’s link graph as a whole — obsidian help | grep -i graph matches nothing — so anything that reasons about the vault’s structure has to rebuild the graph one note at a time
On a vault of 5512 files, 1296 of them markdown, that is:
$ obsidian files | grep '\.md$' > list
$ while IFS= read -r f; do obsidian links path="$f" </dev/null; done < list | wc -l
7161
1296 calls, about 1.5 seconds, and 7161 lines that cannot be attributed afterwards: links prints target paths only, with no column naming the file the call was about. The caller has to remember which call produced which run of lines, and a single mis-step silently reassigns every edge after it. The obvious loop is also wrong for a second reason — without </dev/null the CLI consumes the list from stdin and the loop ends after one file, with no error and a plausible-looking result
The app already holds the finished structure. app.metadataCache.resolvedLinks is an object of source path to an object of target path to link count — the same data the graph view draws — and returning it costs one call
Proposed solution
A graph command that emits the whole thing:
$ obsidian graph format=tsv
02. Projects/Tunnel.md 02. Projects/Tunnel/Client and profiles.md 1
02. Projects/Tunnel.md 00. Attachments/diagram.png 2
$ obsidian graph format=json
{"02. Projects/Tunnel.md":{"02. Projects/Tunnel/Client and profiles.md":1}}
Worth having alongside it:
unresolvedto emitunresolvedLinksin the same shape, so broken targets can be told from real files without a second passtotalfor the edge count, matching the convention every other listing followsnotesto leave attachments out, since an image linked from forty notes is an edge in the index but rarely one in the question being asked
format=tsv matters more than JSON here: the graph is roughly 1 MB of JSON for 1300 notes, and a tab-separated triple per line streams through grep, sort and awk without a parser or a full read into memory
Current workaround (optional)
eval, which reaches the same object in one call and is what the CLI would be exposing:
obsidian eval code='(() => JSON.stringify(app.metadataCache.resolvedLinks))()'
It works, and it is 6 ms against 1.5 s for the loop, but it asks every script that wants the vault’s structure to run JavaScript inside the running app for data the app already has ready
Related feature requests (optional)
- “Structured JSON Output for Obsidian CLI” (topic 112869)
aliaseshas the same shape of gap on a smaller scale: “CLI: givealiasesaformat=option, and its verbose output an array of paths” (topic 119160)