Tools
Graphify
A knowledge graph over a folder of code and documents: what it is, how it is installed and wired to OpenRouter, how to run it without breaking a book, and what it does and does not deliver on this content.
Suggest an editGraphify
Graphify turns a folder of files into a queryable knowledge graph. Code is parsed locally with tree-sitter — deterministic, free, nothing leaves the machine. Documents, PDFs, and images go through a language model for a semantic pass. Every edge is tagged EXTRACTED (explicit in the source) or INFERRED (resolved by the tool), so you can tell what was read from what was guessed.
- Source: Graphify-Labs/graphify
- Package:
graphifyyon PyPI — note the doubley - Site: graphify.com
Why it was installed
The catalog is around 500 Markdown lessons across eight books plus the application repository. Answering "where do I cover consistent hashing?" or "which concepts in this book connect to nothing?" by grepping means reading files into context. A graph answers the second question outright and is meant to answer the first for a few thousand tokens.
The honest result is below in What it delivers. Read that before investing further.
Installation
The CLI is a Python tool. It is installed with uv, which keeps it in its own environment:
uv tool install graphifyyThe CLI then registers its skill with Claude Code:
graphify installThat writes ~/.claude/skills/graphify/SKILL.md and a references/ directory beside it, and registers the /graphify trigger in ~/.claude/CLAUDE.md. Re-run graphify install after every CLI upgrade. The CLI and the skill are versioned separately, and when they drift the CLI warns on every invocation:
warning: skill is from graphify 0.8.16, package is 0.8.35. Run 'graphify install' to update.Check the installed state at any time:
graphify --versionA clean result prints only the version.
The two passes, and what each costs
This is the single most important thing to understand before running anything.
| Pass | Files | Engine | Cost |
|---|---|---|---|
| Code | .rs, .ts, .go, .py, … |
tree-sitter AST, local | free |
| Semantic | Markdown, PDF, images, video | a language model | billed per token |
graphify warns "Large corpus" based on total files, which counts code. That number is misleading. The application repository reported 580 files and ~355,000 words; only 32 of those files were documents, about 39,000 tokens. The measurement that matters is the size of the document bucket.
Measure it before any run that bills:
cd <repo> && /Users/aniket/.local/share/uv/tools/graphifyy/bin/python3 -c "
from graphify.detect import detect
from graphify.llm import _estimate_file_tokens
from pathlib import Path
r = detect(Path('.'))
for k, v in r['files'].items(): print(f' {k}: {len(v)}')
d = r['files']['document'] + r['files']['paper']
t = sum(_estimate_file_tokens(Path(p)) for p in d)
print(f'{len(d)} docs, ~{t:,} tokens, 3.8-flash input ~\${t*0.75/1e6:.2f}')
"⚠️ Never run Graphify on synapse-content without an ignore file. That repository is 34 GB. Its _media/ tree holds 12,083 images and 870 videos. Images go through vision — thousands of extra requests — and videos go through Whisper transcription locally, which takes hours to days before a single token is sent. The text-only cost estimate of ~$10 excluded all of it. A .graphifyignore listing _media/ and local-only-content/ is mandatory there.
Backends and the OpenRouter provider
Graphify ships several backends — gemini, openai, deepseek, kimi, claude, ollama, azure, bedrock, claude-cli — and reads a corresponding API key for each. It reads no other keys: if none of its own variables is set, the Claude Code skill falls back to dispatching subagents from the host session, which is both the most expensive path and the one that collides with this machine's rule against unrequested subagents.
This setup routes through OpenRouter instead, as a custom provider. Custom providers are declared in ~/.graphify/providers.json. The global file is trusted; a project-local .graphify/providers.json is ignored unless GRAPHIFY_ALLOW_LOCAL_PROVIDERS=1 is set, because a provider file controls where the corpus and the key are sent.
{
"openrouter": {
"base_url": "https://openrouter.ai/api/v1",
"default_model": "google/gemini-3.8-flash",
"env_key": "OPENROUTER_API_KEY",
"model_env_key": "GRAPHIFY_OPENROUTER_MODEL",
"pricing": { "input": 0.75, "output": 3.75 },
"temperature": 0,
"max_completion_tokens": 16384,
"vision": true,
"reasoning_effort": "low"
}
}The key is read from OPENROUTER_API_KEY. Export it from the shell profile; never write it into this file.
📘 reasoning_effort: "low" is load-bearing, not a tuning preference. Community labelling sends a prompt with a tiny output budget — min(40 + 16 × communities, 4096) tokens, often under 400. Gemini Flash is a thinking model. Without this setting, reasoning consumes the whole budget, the reply is cut off mid-JSON ({"0":"), and the parser fails with Expecting property name enclosed in double quotes. Every community then silently falls back to the placeholder Community N. Graphify's own built-in gemini backend carries the same setting for the same reason.
pricing is cosmetic. Graphify uses it only for the cost line it prints; it bills nothing. Change the model and the reported cost is wrong until this is updated too.
Choosing a model
Live OpenRouter pricing at the time of setup, USD per million tokens:
| Model | Input | Output | Context |
|---|---|---|---|
google/gemini-3.8-flash |
0.75 | 3.75 | 1M |
google/gemini-3.5-flash-lite |
0.30 | 2.50 | 1M |
google/gemini-3.1-flash-lite |
0.25 | 1.50 | 1M |
:batch variants exist at roughly half price with asynchronous latency, fine for one-shot builds and wrong for incremental updates.
The default is 3.8-flash. For a corpus under about 100,000 document tokens the choice does not matter — the whole run costs cents. Above that, smoke-test 3.1-flash-lite on one small book and compare the reports before committing. Extraction is rubric-following, which small models do well, but that is the tool's claim and was not measured on this content.
Commands
Building a graph
graphify extract . --backend openrouterHeadless, full pipeline: detect, AST on code, semantic pass on documents, merge, cluster. Writes graph.json and manifest.json only. It does not produce the report or the HTML. extract takes exactly one path; there is no multi-path form.
Add --out DIR to write elsewhere. Note that --out DIR creates DIR/graphify-out/, one level deeper than the argument.
Naming communities and generating the report
graphify cluster-only . --graph <path-to>/graph.json --backend=openrouterRe-clusters, names every community with the model, and writes GRAPH_REPORT.md and graph.html. label does the same without re-clustering but has no --graph flag, so it can only operate on <path>/graphify-out/.
⚠️ Two flag syntaxes, and the wrong one fails silently. extract takes --backend openrouter with a space. cluster-only and label take --backend=openrouter with an equals sign. Pass the space form to label and it is ignored; the tool auto-detects a backend from whatever keys are in the environment, picks claude because ANTHROPIC_API_KEY is set, and fails with the 'anthropic' package is required — while still exiting 0 and writing a report full of Community N.
Querying
All three take --graph and default to graphify-out/graph.json in the current directory:
graphify query "how does the authoring pipeline reach the forge?" --graph ~/graphify/synapse/graph.jsongraphify path "Sharding" "Replication" --graph ~/graphify/system-design-guide/graph.jsongraphify explain "Dependency Injection" --graph ~/graphify/low-level-design-guide/graph.jsonquery traverses breadth-first by default (--dfs for a single path) and caps output at --budget N tokens, default 2000.
Keeping a graph current
graphify update .Re-extracts changed code files only. No model, no cost. Document changes need extract … --update, which is incremental but bills.
A post-commit hook does this automatically:
graphify hook installDetails under Hooks.
The output directory and the book trap
Graphify writes graphify-out/ beside whatever it scans. In this content system that is dangerous in a specific way.
A satellite book's repository root is the book. The catalog walker turns every root-level directory into a chapter unless its name is not a valid slug — _media, .git, and not a slug! are skipped; graphify-out is lowercase and hyphenated, a perfectly valid slug, and rendered as a chapter titled "GRAPHIFY OUT" the moment it existed. A .gitignore entry does not help, because the walker reads the working tree, not git.
The spine (synapse-content) is different: there a directory is a book only if it holds book.json, so stray output does not render. It is still 588 KB of junk one git add -A from git-syncing to production.
The rule: for the guide books, output goes to ~/graphify/<repo>/, entirely outside the checkout. For the application repository, which is not a book, graphify-out/ stays in place and is gitignored. Every guide book also carries a .graphifyignore with a single line, book.json, because the book manifest was being parsed as data and producing one meaningless node per key.
| Repository | Output location | Why |
|---|---|---|
Guide books (dsa-guide, sql-guide, …) |
~/graphify/<repo>/ |
root is the book; anything inside renders |
synapse |
synapse/graphify-out/ |
not a book; gitignored; hooks need it in place |
synapse-content |
not yet decided | untracked output exists there today |
Hooks
graphify hook install installs two git hooks, both code-only and free:
- post-commit diffs
HEAD~1..HEAD, re-runs AST on the changed code files, and rebuilds the graph incrementally. - post-checkout fires on branch switches only and does a full code rebuild. It exits immediately if
graphify-out/does not exist, which means a freshgit worktree add— where the gitignored directory is absent — never triggers it.
Both run detached: git commit returns at once and the rebuild proceeds in the background with a 600-second timeout. Both skip during rebase, merge, and cherry-pick. GRAPHIFY_SKIP_HOOK=1 bypasses the commit hook. Output goes to ~/.cache/graphify-rebuild.log, which is shared across every repository with the hook — read the repo name in each entry before drawing conclusions.
The hooks are installed on synapse only. They write to <repo>/graphify-out/ and take no path override, so on a guide book they would recreate the exact directory the book trap is about.
What it delivers
Graphify was run on nine repositories. It executed correctly every time. The result on this content is mixed, and it is better to know that than to discover it.
What works: GRAPH_REPORT.md as a one-off content audit. Community groupings were coherent and correctly named — "Software Design Principles", "Multithreading and Concurrency", "UML Object Relationships". The Knowledge Gaps section lists concepts with one or zero connections, which for a learning platform is a curriculum defect: a term the reader meets and cannot follow anywhere. Extraction quality markers were strong: 95% EXTRACTED, inferred edges at 0.93 confidence.
What does not work: the query commands. On system-design-guide, the densest graph built here at 2,304 nodes:
- 45% of nodes are code symbols —
build_shards(),ConsistentHashRing— lifted from fenced code blocks by the AST pass. - 40% of nodes have one or zero connections.
- 228 node labels are whole prose sentences stored as concepts.
Consistent Hashing, which the book plainly teaches, is not a node.ConsistentHashRing, the class in its example code, is — twice, undeduplicated.
The cause is the corpus shape. Graphify assumes code files and document files are separate. Markdown that interleaves prose with substantial code examples gets both treatments, and the AST half floods the graph with symbols that outnumber the concepts. --mode deep, documented as producing richer inferred edges, was tested on the same book and changed nothing measurable: isolated nodes 40.5% → 40.2%, zero inferred edges either way, same cost.
Extraction is also not reproducible. Re-running an unchanged small book moved edge counts by 26–34% and made a named concept (Repository Pattern) vanish outright. Large corpora are stable; small ones are not.
Recommendation: treat the reports as a content audit worth doing once per book. Do not build a workflow on query, path, or explain for this content until the code-block pollution is addressed, which would need a preprocessing step the tool does not provide.
Do and do not
Do
- Measure the document bucket before any billed run.
- Pass
--backend openroutertoextracton every run. The skill's auto-detect only recognises its own built-in backends, so a custom provider is invisible to it and a bare run falls back to subagents. - Keep
.graphifyignorein every book with at leastbook.json. - Write guide-book output to
~/graphify/<repo>/, never into the checkout. - Re-run
graphify installafteruv tool upgrade graphifyy.
Do not
- Do not run against
synapse-contentwithout_media/andlocal-only-content/ignored. - Do not pass
--backend openrouterwith a space tocluster-onlyorlabel. - Do not trust
pricingin the provider file as a bill; it is a label. - Do not install the hook on a book repository.
- Do not read a single graph's edge count as a quality measure; it moves between runs.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
warning: skill is from graphify X, package is Y |
CLI upgraded, skill not re-registered | graphify install |
labeling failed (the 'anthropic' package is required …) |
--backend given with a space to label/cluster-only; flag ignored, backend auto-detected |
Use --backend=openrouter |
labeling failed (Expecting property name enclosed in double quotes …) |
Model output truncated by the small label budget; reasoning ate it | Add "reasoning_effort": "low" to the provider |
Report full of Community N |
One of the two above; the tool degrades silently and still exits 0 | Fix the cause, re-run cluster-only |
error: no graph found at DIR/graph.json |
--out DIR nests one level: the graph is at DIR/graphify-out/graph.json |
Point --graph at the nested path |
| A chapter named "GRAPHIFY OUT" appears in a book | Output written inside a satellite repo; the walker rendered it | Move it to ~/graphify/<repo>/; a gitignore entry does not help |
No API key for backend 'openrouter' |
OPENROUTER_API_KEY unset in this shell |
Export it; check with [ -n "$OPENROUTER_API_KEY" ], never echo |
Unknown backend 'openrouter' |
~/.graphify/providers.json missing or malformed |
Verify with the Python snippet below |
| "Large corpus" warning on a code repo | Counts code files, which cost nothing | Check the document bucket instead |
| Query returns two or three nodes | Sparse graph, the content-shape problem above | Read GRAPH_REPORT.md instead |
Verify the provider is registered without running anything billed:
/Users/aniket/.local/share/uv/tools/graphifyy/bin/python3 -c "
from graphify.llm import BACKENDS, _backend_env_keys, _default_model_for_backend
print('registered:', 'openrouter' in BACKENDS)
print('env keys :', _backend_env_keys('openrouter'))
print('model :', _default_model_for_backend('openrouter'))
"Verify the label call itself completes, cheaply and directly, before blaming Graphify:
/Users/aniket/.local/share/uv/tools/graphifyy/bin/python3 -c "
import os
from openai import OpenAI
c = OpenAI(api_key=os.environ['OPENROUTER_API_KEY'], base_url='https://openrouter.ai/api/v1')
r = c.chat.completions.create(model='google/gemini-3.8-flash', reasoning_effort='low',
messages=[{'role':'user','content':'Reply with the JSON object {\"ok\": true} and nothing else.'}],
max_completion_tokens=40)
print(r.choices[0].finish_reason, repr(r.choices[0].message.content))
"stop with valid JSON is healthy. length with a truncated string is the reasoning-budget problem.
Measured costs
For calibration, from the runs that built this setup, all at google/gemini-3.8-flash:
| Corpus | Documents | Input tokens | Cost |
|---|---|---|---|
synapse (application) |
32 | ~39,000 | ~$0.10 |
low-level-design-guide |
23 | 93,935 | $0.09 |
system-design-guide |
112 | 415,561 | $0.45 |
| All eight guide books, extract + label | 504 | ~1.4M | ~$1.36 |
Cheap enough that the cost is never the reason not to run it. The reason, when there is one, is the result.