Navigating a Neo4j Knowledge Graph with Jev

Photo of Michael Hunger

Michael Hunger

Head of Product Innovation & Developer Strategy, Neo4j

Testing Jev (TypeSafe System One) with Navigation Decisons

I built neo4jev as an experiment to navigate a graph, here I explain a bit the why and how.

Streamlit app in neo4jev that allows database config, source selection and different target modes with tracing of each decision
Streamlit app in neo4jev that allows database config, source selection and different target modes with tracing of each decision

What is Jev? And why should you care?

Last week’s Typsafe.AI announcement of their Jev decision and classification model has taken the AI engineering community by storm.

Jev is the fastest-adopted model in AI Gateway history

It is not an LLM it’s a fast, small and cheap ($42 per billion input tokens, output free) classification model that only outputs probabilities for Choice, Boolean Truth Decisions (Noul), and Scores with text and structured inputs but no text output. It doesn’t do single token at a time prediciton, but runs all predictions in parallel, so you can pass as many tasks as you want into a single API call.

The model is not trained on RLHF for human alignment, but RLCD (RL for Calibrated Decisions) — so it is more like traditional ML tasks, just with a more modern model base.

The docs are good, there is a Playground, and official SDKs for JavaScript and Python and inofficial ones for any other language. It has already been integrated into OpenRouter, Pydantic.ai (great docs), LangChain, BrainTrust (for judging)

These kind of models are meant to be used inside the hot loop of software, where you usually do if/switch decisions on data that are not deterministic but probabilistic. Current LLMs are too slow and expensive for that.

Neo4Jev — Graph Navigation with Jev

When a new model API or paradigm comes out, the toy examples usually work fine, look nice but don’t give you hands-on experience.

While waiting for my waiting list submission to go through I wanted to try something close to my heart and easy to implement and demonstrate. Navigating an existing Neo4j knowledge graph, with real data, real schema, a task where you can see whether the thing actually works or just sounds convincing.

I got the idea from their “WikiRacing/WikiGame” demo, just to have something doing meaningful work.

Wikiracing

The objective of the game is to start on one Wikipedia page and reach a specific other Wikipedia page using only links you come across while traversing. Each step can mean choosing between hundreds to thousands of links! It’s a great playground for demonstrating not just intelligence-per-second, but also the compounding benefits of not hallucinating with high-cardinality choices.

The task: graph navigation. Start at a node, follow relationships toward a goal (target entity, path goal or general task), one hop at a time, and have the model — not hand-written heuristics — decide which relationship to follow at each step.

For testing I used Neo4j’s public Companies knowledge graph (15 labels, 28 relationship types) available on https://demo.neo4jlabs.com:7473 in the “companies2” database.

Why navigation is a useful test case

If you try to use LLM for graph traversals, i.e. predicting a path through a dataset — they do not great, as the generated paths, nodes, or relationships might actually not exist.

They are quite good at query generation turning a user question with a graph schema into a graph query, which we use in Text2Cypher.

But for this use-case of interactive navigation, they are too slow. Traversal at any given node is not a generation problem. It is a choice between of the current state, (possibly history) and a concrete, finite set of options: the relationships that actually leave this node. That fits an API that returns probabilities over options much better than one that returns prose.

How does Jev operate

The system_one API takes a state (arbitrary JSON describing the situation) and a set of questions, and returns answers with probabilities. The question types that matter here:

  • Choice — “which of these options?” You pass a map of keys to descriptions. The answer includes the winner, a confidence score, and the full probability distribution over every option.
  • Noul — a 0..1 float, basically “how true is this statement?” We use it as a goal-reached check.
  • Score — numeric scoring, not needed here.

You can ask several questions in one call (they run in parallel, no extra latency or cost), so one round-trip per hop both picks the next edge and checks whether we’ve arrived. And a beam search needs what Choice returns: a distribution to branch on.

Start somewhere and follow the trace

The database config and typesafe API key are loaded from a .env file.

The neo4j connector loads the schema, for start node selection you pick a label and then can find entries via exact, fulltext or vector search.

E.g. in our case an Company like Apple

Then you specify a target mode

  • Reaching a target node, e.g. a board member like Larry Page
  • A path intent, conceptually follow this kind of path, e.g. find articles mentioning this company
  • A free text task, e.g. find all people who are on the board of this company and the other companies they are advicing

Then the app iterates a loop, until the threshold

  1. From the current node, fetch its outgoing and incoming relationships — type, properties, and the label + properties of the target node on the other side.
  2. Present them as Choice options: “which relationship should the traversal follow next?” Option keys are opaque ids (e0, e1, …) with a side table back to the real edges.
  3. In the same call, ask a Noul: “has the goal been reached at the current node?”
  4. Follow the top-k options above a probability cutoff, branch, repeat.

Each branch accumulates the sum of log-probabilities of its edges (log-sums avoid underflow and don’t penalize longer paths the way products do).

    candidates = list(fetch_candidates(state.node_id))
budget.spend()
view = goal_view(goal, state.depth)
hop = await _execute_hop(
client, node, candidates, view,
settings=config, hop_index=state.depth,
path=state.path,model=None)
expansion = _Expansion(candidates=hop.candidates)

if view.goal_reached_by_noul(hop.noul, config.goal_threshold):
expansion.terminated.append(
NavPath(
steps=[*state.path, _terminal_step(state.node_id, hop)],
cumulative_log_prob=state.cumulative_log_prob,
terminated_reason=TerminationReason.GOAL_REACHED,
)
)
return expansion

for candidate, probability in hop.chosen:
next_id = candidate.next_element_id
if not next_id or next_id in state.visited:
continue
step = NavStep(
node_id=state.node_id,chosen=[candidate],
probabilities=hop.probabilities,
log_prob=math.log(max(probability, _LOG_FLOOR)),
candidates=hop.candidates,noul=hop.noul)

child = state.extend(step, next_id)
if view.is_target(next_id):
expansion.terminated.append(
NavPath(
steps=list(child.path),
cumulative_log_prob=child.cumulative_log_prob,
terminated_reason=TerminationReason.GOAL_REACHED,
)
)
else:
expansion.children.append(child)
expansion.nodes[next_id] = NodeContext(
element_id=next_id,
label=candidate.next_label,
properties=candidate.next_props,
)

if not expansion.children and not expansion.terminated:
expansion.terminated.append(_as_path(state, TerminationReason.NO_CANDIDATES))
return expansion

A visited set blocks cycles, max depth,and an API-call budget keep the search finite, a beam width (neighbours) caps live branches to the max Choice entries count.

The result is the best-scoring path or paths, plus the explored-but-not-taken branches as context.

A dead-end node still gets its call — the Noul alone, since an empty Choice has nothing to pick. So “reached the goal and stopped” is distinguishable from “ran out of edges”.

How to reach the the goal based on target mode

  • Free text — “find a company that competes with the start node.” The description goes into the state, the Noul checks it each hop.
  • Target node — navigate to a specific node, picked via a lookup UI. The Noul is trivial here (“is this it?”); the Choice does the wayfinding.
  • Path intent — describe the shape of the path rather than a destination: “from the patient to the gene expressions for their diseases.” The description is split into stages that bias each hop’s Choice; the Noul checks the final stage. This mode returns the top-N paths.

What actually mattered

The beam search was the easy part. These decisions had much more effect on whether Jev performed well or badly:

The model can only choose between options it can read. Ensure to send correct and sensible data to the model, and not opaque identifiers.

Send the schema as a “map” along with the local edges. A hop’s candidates only show what’s at the current node. Without more context, the model can’t reason about a detour — e.g. going via a news article to reach a competitor when there’s no direct edge. It would go along tangents (currently no backtracking)

Keep the payload reasonably small. Long text properties and embedding vectors bloat the request and bury the fields that matter. Smaller state, better probabilities, lower cost.

Offer incoming edges too. Initially only outgoing edges were candidates, which made certain nodes unreachable. The traversal now offers both directions, tagged explicitly, so the model can walk backwards along an edge when the goal is upstream.

Cap candidates fairly. Real graphs are uneven, skewed — one company has thousands of supplier edges and a handful of competitors. A naive total cap starves the rarer, more interesting relationship types. We cap per edge type first, then fill the remaining budget round-robin, and report “N of M edges considered” so truncation is visible.

The system

The code around the core pattern is minimal, just 5 python files.

https://github.com/jexp/neo4jev/tree/main/src/neo4jev

  • neo4j_access — everything Neo4j: label and index introspection, candidate fetching with the capping above, identity-property derivation, the schema snapshot.
  • types — static types for holding the graph results and the choices and API payload for the Typsafe API
  • navigator — the beam search and the TypeSafe boundary: builds the per-hop system_one call, turns probabilities into branches, scores paths.
  • viz — renders paths and neighborhood context with neo4j-viz, colored by label/type, path colors on top, with legends, details, layots provided out of the box
  • Notebooks — three notebooks (explore, single-hop, full traversal over all goal modes) and
  • Streamlit app — an app that shows the per-hop trace: every option Jev was offered with its probability, what it picked, and the Noul verdict.

Does the idea hold up?

So far, yes, with caveats. When the payload is clean and the schema is included, Jev navigates sensibly: From Microsoft, asked to find a competitor, it takes HAS_COMPETITOR → Amazon.com, Inc. at p 0.85 — and the full distribution shows why, which no prose answer would. The three goal modes all produce coherent paths.

The caveats are:

  • decision quality tracks payload quality closely (garbage properties in, indifferent choices out),
  • high-degree nodes need capping to stay within the 255-option Choice limit and reasonable token cost, and
  • the goal-reached Noul is the weakest link for vague free-text goals.

The broader point stands: if each step of a task can be phrased as a decision over real, fetched options, you don’t need to let a model improvise queries or narrate a walk.

You get traversal that is

  • grounded (every edge walked exists),
  • inspectable (the probability distribution is recorded for every hop),
  • steerable (goals are just state), and
  • cheap (one API call per hop, questions multiplexed within it).

That pattern should transfer to query planning, workflow routing, model/tool choices, and other kinds of decision making. Graph navigation is just a very literal version of it.

Try it

GitHub Repository is here: https://github.com/jexp/neo4jev

The demo runs against Neo4j’s public Companies knowledge graph

Point .env at any other Neo4j instance and labels, indexes, identity properties and schema are all discovered live.

The sky is the limit

They have a waiting list but you get in within a day. You should definitely join their Discord, it’s super active, they have regular town hall AMAs and a very interesting show-and-tell channel with thousands of demos and applications.

Some things I wanted to try out are:

  • Entity Resolution (my colleague Kervin Hu wrote about it

I tested Jev on entity resolution

  • Tool routing/selection (incl. parameter extraction)
  • Decision making for coding and database harnesses (e.g. for compaction, model routing, determining criticality of changes, code-review)

I had Jev rank some ideas (from Claude) for using decision models, it’s what you expect, typical decision taking that you need in code, but more dynamic than deterministic code could give us.

Soo many sites listing Jev projects.

There are already a lot of Alternatives/Clones like Laya (MLX), BERT, Qwen LoRA trained (Kev) decision models, and OpenJev.

If you haven’t, go try it out!


Navigating a Neo4j Knowledge Graph with Jev was originally published in Neo4j Developer Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.