FalkorDB
Solutions
Use cases
Why FalkorDB
Developers

A Code Knowledge Graph Cut Claude Code’s Token Use 25%. Here’s How to Build One

Code knowledge graph: a highlighted line in a code editor leads through a call graph to the one test that covers it

Coding agents spend a lot of their time searching: grepping for callers, opening files to find the right tests, rereading code they already saw. At Graph Hacks, FalkorDB's online hackathon (October 15 to 18, 2026, $20K in prizes), which drew 7,899 developers and 1,213 projects, one builder gave Claude Code a FalkorDB map of a real repo. Across 72 runs it used 25% fewer input tokens and 29% fewer tool calls. Another builder used the same kind of graph to pick about 36 of click's 2,209 tests per change and still caught 30 of 31 planted bugs. Here's how to build a code knowledge graph on a free FalkorDB Cloud instance and connect it to Claude Code, in six steps.

Key takeaways. A code knowledge graph lets an agent ask what a change touches before it edits. In six steps you load a real Python repo into FalkorDB with about 80 lines of Python, ask three read-only questions, rank load-bearing functions with algo.pageRank, and connect Claude Code through the FalkorDB MCP server in strict read-only mode. Every snippet was run against FalkorDB, and the output shown is real.

Follow along for free. Every step runs on a free FalkorDB Cloud instance, with no credit card. Start free on FalkorDB Cloud and keep the tab open for step 1.

What is a code knowledge graph?

A code knowledge graph stores a repository's files, functions and tests as nodes, and calls, test coverage and co-changes as edges, so an agent can ask what a change touches.

Claude Code is great at reading and editing files, but files don't show the structure between them. In 83 of Graph Hacks builder Manoj Shetty's own Claude Code sessions, 31% of 32,713 tool calls were spent searching or navigating. A graph answers those questions with one query each.

Question before an editText searchCode graph on FalkorDB
What calls this function?Grep, then read every hitOne CALLS*1..3 query
Which tests reach it?Guess from file namesWalk back from the function
What changes with this file?Read git log by handCO_CHANGED edges
Which code is load-bearing?No direct answeralgo.pageRank
What you'll need. Python 3, git, Node.js (for npx), Claude Code, and a free FalkorDB Cloud account.
The six steps. 1. Create a free Cloud instance · 2. Pick a repo · 3. Load the graph · 4. Ask questions · 5. Rank with PageRank · 6. Connect Claude Code

Step 1. Create a free FalkorDB Cloud instance

Create your free FalkorDB Cloud instance

No credit card needed. In the console, open Deployments, then Instances, and select Create. Pick the FalkorDB Free plan, a provider and a region, and set a password. When the instance shows Running, copy the host and port from its Connectivity tab. The port is often not 6379, and the username is falkordb. A Free instance stops after 1 day without use and is deleted after 7 days, so re-run load.py if your graph is gone.

export FALKORDB_HOST="<host>"
export FALKORDB_PORT="<port>"
export FALKORDB_USERNAME="falkordb"
export FALKORDB_PASSWORD="<password>"

Step 2. Pick a repo for your code knowledge graph

We'll map pallets/itsdangerous, a small library from the team behind Flask and click, with 8 source files and 677 commits at the time of writing. Any Python repo works. Clone the full history, because co-change comes from git log.

pip install falkordb
git clone https://github.com/pallets/itsdangerous

Step 3. Model the code knowledge graph and load it into FalkorDB

EdgeConnectsSource
DEFINESFile to FunctionPython's ast module
CALLSFunction to FunctionCalls in the body, matched by name
COVERSTest to FunctionCalls in each test_ function
CO_CHANGEDFile to FileEdited together in 3 or more commits

Save this as load.py next to the itsdangerous folder and run python load.py. It parses the code without running it, reads the git history, and loads everything in batches with UNWIND. It rebuilds the graph named code on every run.

import ast, os, subprocess
from collections import Counter
from itertools import combinations
from pathlib import Path
from falkordb import FalkorDB

REPO = Path("itsdangerous")

def functions(path):
    """Every function and method in a file, with its qualified name."""
    def walk(node, prefix):
        for child in ast.iter_child_nodes(node):
            if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
                yield prefix + child.name, child
            elif isinstance(child, ast.ClassDef):
                yield from walk(child, prefix + child.name + ".")
    yield from walk(ast.parse(path.read_text()), "")

def called_names(fn):
    """Names a function calls: foo() and obj.foo() both give 'foo'."""
    for n in ast.walk(fn):
        if isinstance(n, ast.Call):
            if isinstance(n.func, ast.Name):
                yield n.func.id
            elif isinstance(n.func, ast.Attribute):
                yield n.func.attr

files, funcs, tests, calls, covers = [], {}, {}, set(), set()
for path in sorted(REPO.glob("src/**/*.py")):
    rel = str(path.relative_to(REPO))
    files.append(rel)
    for qualname, fn in functions(path):
        fid = f"{rel}::{qualname}"
        funcs[fid] = {"id": fid, "name": fn.name, "file": rel}
        calls |= {(fid, name) for name in called_names(fn)}
for path in sorted(REPO.glob("tests/**/test_*.py")):
    rel = str(path.relative_to(REPO))
    for qualname, fn in functions(path):
        if fn.name.startswith("test_"):
            tid = f"{rel}::{qualname}"
            tests[tid] = {"id": tid, "file": rel}
            covers |= {(tid, name) for name in called_names(fn)}

# Co-change: pairs of source files edited in the same commit, counted over git history.
log = subprocess.run(["git", "-C", str(REPO), "log", "--name-only", "--pretty=format:@@"],
                     capture_output=True, text=True, check=True).stdout
pairs = Counter()
for commit in log.split("@@"):
    changed = sorted({p for p in commit.splitlines() if p in files})
    pairs.update(combinations(changed, 2))

db = FalkorDB(host=os.environ["FALKORDB_HOST"], port=int(os.environ["FALKORDB_PORT"]),
              username=os.environ["FALKORDB_USERNAME"], password=os.environ["FALKORDB_PASSWORD"])
if "code" in db.list_graphs():
    db.select_graph("code").delete()
g = db.select_graph("code")
g.query("CREATE INDEX FOR (f:Function) ON (f.id, f.name)")
g.query("CREATE INDEX FOR (f:File) ON (f.path)")
g.query("CREATE INDEX FOR (t:Test) ON (t.id)")

def load(cypher, rows):
    g.query("UNWIND $rows AS r " + cypher, {"rows": rows})

load("CREATE (:File {path: r})", files)
load("""MATCH (file:File {path: r.file})
        CREATE (file)-[:DEFINES]->(:Function {id: r.id, name: r.name, file: r.file})""",
     list(funcs.values()))
load("CREATE (:Test {id: r.id, file: r.file})", list(tests.values()))
# Calls are resolved by name, so this is an approximate call graph. Dunder names are skipped.
load("""MATCH (a:Function {id: r.src}), (b:Function {name: r.name})
        WHERE a <> b AND NOT r.name STARTS WITH '__' CREATE (a)-[:CALLS]->(b)""",
     [{"src": s, "name": n} for s, n in calls])
load("""MATCH (t:Test {id: r.src}), (f:Function {name: r.name})
        WHERE NOT r.name STARTS WITH '__' CREATE (t)-[:COVERS]->(f)""",
     [{"src": s, "name": n} for s, n in covers])
load("""MATCH (a:File {path: r.a}), (b:File {path: r.b})
        CREATE (a)-[:CO_CHANGED {commits: r.n}]->(b)""",
     [{"a": a, "b": b, "n": n} for (a, b), n in pairs.items() if n >= 3])

print(g.query("MATCH (n) RETURN labels(n)[0] AS label, count(n) ORDER BY label").result_set)
print(g.query("MATCH ()-[r]->() RETURN type(r) AS type, count(r) ORDER BY type").result_set)
[['File', 8], ['Function', 54], ['Test', 37]]
[['CALLS', 109], ['COVERS', 185], ['CO_CHANGED', 22], ['DEFINES', 54]]

That's 99 nodes and 370 edges. Calls are matched by name, so the call graph is approximate, which is fine for steering an agent. For exact COVERS edges, record per-test coverage, as ImpactGraph does below. The queries stay the same.

Step 4. Ask the code knowledge graph three read-only questions

Say you're about to change Signer.derive_key. What calls it, which tests reach it, and what usually changes with signer.py? Save this as ask.py and run it.

import os
from falkordb import FalkorDB

db = FalkorDB(host=os.environ["FALKORDB_HOST"], port=int(os.environ["FALKORDB_PORT"]),
              username=os.environ["FALKORDB_USERNAME"], password=os.environ["FALKORDB_PASSWORD"])
g = db.select_graph("code")

WHAT_CALLS = """
MATCH p = (caller:Function)-[:CALLS*1..3]->(:Function {name: $name})
RETURN caller.id AS caller, min(length(p)) AS hops
ORDER BY hops, caller LIMIT 5"""

WHICH_TESTS = """
MATCH p = (t:Test)-[:COVERS]->(:Function)-[:CALLS*0..2]->(:Function {name: $name})
RETURN t.id AS test, min(length(p)) AS hops
ORDER BY hops, test LIMIT 5"""

CHANGES_WITH = """
MATCH (:File {path: $path})-[c:CO_CHANGED]-(other:File)
RETURN other.path AS file, c.commits AS commits
ORDER BY commits DESC LIMIT 3"""

for title, query, params in [
    ("What calls derive_key?", WHAT_CALLS, {"name": "derive_key"}),
    ("Which tests reach derive_key?", WHICH_TESTS, {"name": "derive_key"}),
    ("What changes with signer.py?", CHANGES_WITH, {"path": "src/itsdangerous/signer.py"}),
]:
    print(title)
    for row in g.ro_query(query, params).result_set:
        print("  ", *row)
What calls derive_key?
   src/itsdangerous/signer.py::Signer.get_signature 1
   src/itsdangerous/signer.py::Signer.verify_signature 1
   src/itsdangerous/signer.py::Signer.sign 2
   src/itsdangerous/signer.py::Signer.unsign 2
   src/itsdangerous/signer.py::SigningAlgorithm.verify_signature 2
Which tests reach derive_key?
   tests/test_itsdangerous/test_signer.py::TestSigner.test_invalid_key_derivation 1
   tests/test_itsdangerous/test_signer.py::TestSigner.test_broken_signature 2
   tests/test_itsdangerous/test_signer.py::test_abstract_algorithm 2
   tests/test_itsdangerous/test_serializer.py::TestSerializer.test_bad_payload_exception 3
   tests/test_itsdangerous/test_serializer.py::TestSerializer.test_loads_unsafe 3
What changes with signer.py?
   src/itsdangerous/serializer.py 10
   src/itsdangerous/exc.py 6
   src/itsdangerous/timed.py 6

CALLS*1..3 finds callers two hops away, like Signer.sign, that a name search would miss. The second query finds tests that reach the function through other code, ranked by distance. And ro_query sends GRAPH.RO_QUERY, which FalkorDB runs read-only on the server. A CREATE sent through it was refused.

Step 5. Find load-bearing code in the code knowledge graph with PageRank

A change to code that everything else depends on needs the widest test run. FalkorDB runs PageRank inside the database, even in a read-only query.

import os
from falkordb import FalkorDB

db = FalkorDB(host=os.environ["FALKORDB_HOST"], port=int(os.environ["FALKORDB_PORT"]),
              username=os.environ["FALKORDB_USERNAME"], password=os.environ["FALKORDB_PASSWORD"])
g = db.select_graph("code")

LOAD_BEARING = """
CALL algo.pageRank('Function', 'CALLS') YIELD node, score
RETURN node.id AS function, round(score * 1000) / 1000.0 AS score
ORDER BY score DESC LIMIT 5"""

for row in g.ro_query(LOAD_BEARING).result_set:
    print(*row)
src/itsdangerous/encoding.py::want_bytes 0.081
src/itsdangerous/serializer.py::Serializer.make_signer 0.027
src/itsdangerous/serializer.py::Serializer._loads_unsafe_impl 0.022
src/itsdangerous/serializer.py::Serializer.dumps 0.021
src/itsdangerous/serializer.py::_PDataSerializer.dumps 0.021

want_bytes ranks first. 30 functions reach it within three calls, because almost every encode and decode path goes through it. An agent should know that before it touches a helper that looks small.

Step 6. Connect Claude Code to the code knowledge graph through the FalkorDB MCP server

Register the official FalkorDB MCP server from inside your project, with the values from step 1. FALKORDB_DEFAULT_READONLY runs queries with GRAPH.RO_QUERY, and FALKORDB_STRICT_READONLY blocks writes even if the agent asks for one.

claude mcp add falkordb \
  -e FALKORDB_HOST=$FALKORDB_HOST -e FALKORDB_PORT=$FALKORDB_PORT \
  -e FALKORDB_USERNAME=$FALKORDB_USERNAME -e FALKORDB_PASSWORD=$FALKORDB_PASSWORD \
  -e FALKORDB_DEFAULT_READONLY=true -e FALKORDB_STRICT_READONLY=true \
  -- npx -y @falkordb/mcpserver
claude mcp get falkordb
falkordb:
  Scope: Local config (private to you in this project)
  Status: √ Connected
  Type: stdio
  Command: npx
  Args: -y @falkordb/mcpserver

We tested server version 1.3.0 against the step 3 graph. The step 4 callers query returned the same rows through its query_graph_readonly tool, and a write came back with "Cannot execute write queries: server is in strict read-only mode".

Then tell Claude Code the graph exists, with a short section in your project's CLAUDE.md.

## Code graph
The FalkorDB graph named code maps this repo.
(:File)-[:DEFINES]->(:Function), (:Function)-[:CALLS]->(:Function),
(:Test)-[:COVERS]->(:Function), (:File)-[:CO_CHANGED {commits}]->(:File)
Before you change a function, use the falkordb tools to find its callers
and the tests that reach it. Run those tests after the edit.

Now ask "What could break if I change Signer.derive_key, and which tests should I run?" Claude Code can look up callers and tests through query_graph_readonly instead of a round of searching. Re-run load.py from a git hook to stay current. For a ready-made version, FalkorDB's open source CodeGraph indexes Python, Java and C# repos and ships its own MCP server.

What did a code knowledge graph change on pallets/click?

ImpactGraph runs only the tests a change can break

ImpactGraph · Manoj Kengudelu · See the code

Most of click's 2,209 tests never execute a given change, and picking tests by file name misses the ones that reach the code through other functions.

ImpactGraph keeps click's code, tests and history in one FalkorDB graph of 545 functions, 2,209 tests, 360 commits and about 157,600 edges, 152,526 of them per-test COVERS edges from real coverage data. A rule-based ranker and a Claude agent pick tests only through fixed GRAPH.RO_QUERY queries, and every pick comes with the graph path to the change. It runs on embedded FalkorDBLite or on any FalkorDB server, including FalkorDB Cloud.

ImpactGraph demo
Setup (31 planted bugs)Tests run per changeBugs caught
Full suite2,20931
ImpactGraph on FalkorDB35.5 on average30
Random tests, same number35.5 on average12

The selection ran in 0.4 seconds, against 5.9 for the full suite. 83 functions in click have no test, and for 9 of them this query still finds tests through covered callers.

UNWIND $changed AS fid
MATCH (f:Function {id: fid}) WHERE f.tests = 0
MATCH p = (caller:Function)-[:CALLS*1..3]->(f)
WHERE caller.tests > 0
WITH f, caller, min(length(p)) AS hops
MATCH (t:Test)-[:COVERS]->(caller)
RETURN f.id AS function, t.id AS test, t.file AS file, t.breadth AS breadth,
       caller.qualname AS via, hops

Try it. Swap the name-matched COVERS edges from step 3 for per-test coverage, and keep the step 4 queries. Start free on FalkorDB Cloud.

Engram gives Claude Code a map before the first tool call

Engram · Manoj Shetty · See the code

A coding agent greps, reads and greps again until it finds the function, and next session it starts from zero.

Engram turns each repo into its own FalkorDB graph of symbols, calls, commits, co-changes and the agents' own sessions. Before Claude Code's first tool call, it adds a map of about 200 tokens with the likeliest file:lines, from 20 to 90 ms of graph queries and no LLM calls. It uses algo.pageRank, algo.labelPropagation and full-text indexes, and every agent-facing read goes through GRAPH.RO_QUERY.

Engram demo

Across 72 Claude Code runs on 12 click bugs with the same model (36 with Engram, 36 without), Engram cut input tokens by 25%, tool calls by 29% and time by 23%. Search calls fell from 3.4 to 1.7 per bug. Both setups fixed nearly every bug (35 of 36 with Engram, 36 of 36 without). After every edit, this query joins git co-change, the session's edits and the call graph in about 11 ms to name files the agent probably also needs to change.

MATCH (f:File {path: $file})-[k:CO_CHANGES]-(o:File)
WHERE k.count >= 3 AND k.weight >= 0.3 AND NOT o.isTest AND NOT o.isAux
OPTIONAL MATCH (:Session {id: $sid})-[e:EDITED]->(o)
WITH f, o, k, e WHERE e IS NULL
OPTIONAL MATCH (o)-[:DEFINES]->(c:Symbol)-[:CALLS]->(:Symbol {path: $file})
WITH o, k, collect(DISTINCT c.qualname)[0..2] AS dependents
WHERE size(dependents) > 0 OR k.weight >= 0.5
RETURN o.path AS other, k.count AS count, k.weight AS weight, dependents
ORDER BY size(dependents) DESC, k.count DESC LIMIT 2

Engram runs embedded FalkorDBLite on each machine. For a team, its docs say to point everyone at one FalkorDB, such as FalkorDB Cloud.

Try it. Put your team's code graph on one FalkorDB Cloud instance, and every agent reads the same map. Start free on FalkorDB Cloud.

Want to go further? Read 5 agent memory patterns from 647 Graph Hacks projects, or see how teams built a company brain their agents can query.

Frequently asked questions

What is a code knowledge graph?

A code knowledge graph stores a repository's files, functions and tests as nodes, and calls, test coverage and co-changes as edges, so an agent can ask what a change touches. Questions like what calls this function or which tests reach it take one query each.

How do I connect Claude Code to a graph database?

Use an MCP server. For FalkorDB, run claude mcp add falkordb with your connection details as -e variables, then -- npx -y @falkordb/mcpserver. Add FALKORDB_STRICT_READONLY=true so the agent can only read. See the FalkorDB MCP server docs.

Does a code knowledge graph help coding agents?

In a Graph Hacks benchmark of 72 Claude Code runs on pallets/click, a FalkorDB code graph cut input tokens by 25% and tool calls by 29% with the same model. In another, a FalkorDB graph of click picked about 36 of 2,209 tests per change and caught 30 of 31 planted bugs.

Can I build a code knowledge graph on the FalkorDB free tier?

Yes. Every step in this tutorial runs on a free FalkorDB Cloud instance, with no credit card. The example graph of pallets/itsdangerous has 99 nodes and 370 edges.

How do I keep a code knowledge graph up to date?

Re-run the loader when code changes, for example from a git hook or CI. Larger tools re-index only the files that changed.

Give Claude Code a code knowledge graph of your repo

Everything in this tutorial runs on the same FalkorDB you can start with one Docker command, docker run -p 6379:6379 -p 3000:3000 -it --rm falkordb/falkordb:latest, or a free cloud instance. Load one repo and let your agent ask what a change touches before every edit.

Start free on FalkorDB CloudRead the get started guide

Not ready to build yet? Join the FalkorDB Discord to get help from our engineers, share what you build, and hear about the next Graph Hacks first.

Author