How to Use the Traversal API for Graph Traversals in neomodel

Use the traverse() method on any node set to build multi-hop Cypher queries by passing relationship paths as double-underscore separated strings or Path objects.

The Traversal API in the neo4j-contrib/neomodel library provides a Pythonic interface for walking relationship chains in Neo4j without writing raw Cypher. It translates path expressions into optimized MATCH clauses, allowing you to fetch deeply connected data in a single round-trip.

Understanding the Traversal API Architecture

The Traversal API is implemented primarily in neomodel/sync_/match.py (with an identical async counterpart in neomodel/async_/match.py). The system converts Python path descriptors into Cypher query components through the following core classes:

The Path Dataclass

The Path dataclass (defined at line 42 in neomodel/sync_/match.py) encapsulates a single traversal step. It stores the relationship chain string (e.g., "suppliers__country"), flags for optional matching, return selectors, and variable aliases.

BaseSet.traverse Method

BaseSet.traverse (line 25) serves as the public entry point. When called on a node set (e.g., Coffee.nodes), it accepts one or more path arguments and delegates to _register_relation_to_fetch to normalize them into Path instances.

QueryBuilder Integration

The QueryBuilder.build_ast method consumes the collected Path objects from self.relations_to_fetch. It translates each path into a Cypher pattern such as (:Coffee)<-[:SUPPLIES]-(:Supplier)-[:ESTABLISHED_IN]->(:Country), attaching it to the query's abstract syntax tree.

Subgraph Resolution

After execution, BaseSet.resolve_subgraph (line 91) reconstructs the flat Cypher results into a nested Python object graph. This allows access to traversed relationships via the _relations attribute on returned nodes.

Basic Traversal Patterns

Define your models with StructuredNode and relationship types:

from neomodel import StructuredNode, StringProperty, IntegerProperty, RelationshipTo, RelationshipFrom

class Country(StructuredNode):
    country_code = StringProperty(unique_index=True)
    name = StringProperty()

class Supplier(StructuredNode):
    name = StringProperty()
    delivery_cost = IntegerProperty()
    country = RelationshipTo(Country, 'ESTABLISHED_IN')

class Coffee(StructuredNode):
    name = StringProperty(unique_index=True)
    price = IntegerProperty()
    suppliers = RelationshipFrom(Supplier, 'SUPPLIES')

Simple Multi-Hop Traversal

Use double underscores (__) to chain relationships. This example walks from Coffee to Supplier to Country:

coffees = Coffee.nodes.traverse("suppliers__country").all()

This generates Cypher equivalent to:

MATCH (coffee:Coffee)<-[:SUPPLIES]-(supplier:Supplier)-[:ESTABLISHED_IN]->(country:Country)
RETURN coffee, supplier, country

Advanced Traversal Techniques

Controlling Return Values

By default, traverse() returns both nodes and relationships. Use the Path dataclass to customize the return clause:

from neomodel import Path

# Return only nodes, exclude relationships

path = Path(value="suppliers__country", include_rels_in_return=False)
coffees = Coffee.nodes.traverse(path).all()

# Return only relationships, exclude intermediate nodes

path = Path(value="suppliers__country", include_nodes_in_return=False)
coffees = Coffee.nodes.traverse(path).all()

Optional Traversals (LEFT JOIN Equivalent)

Set optional=True to generate an OPTIONAL MATCH clause. This returns source nodes even when the relationship path does not exist:

path = Path(value="suppliers__country", optional=True)
coffees = Coffee.nodes.traverse(path).all()

Generated Cypher:

MATCH (coffee:Coffee)
OPTIONAL MATCH (coffee)<-[:SUPPLIES]-(supplier:Supplier)-[:ESTABLISHED_IN]->(country:Country)
RETURN coffee, supplier, country

Variable Aliasing

Use the alias parameter to assign custom variable names to traversed nodes, useful when referencing them in subsequent filter operations:

path = Path(value="suppliers__country", alias="supplier_country")
coffees = Coffee.nodes.traverse(path).all()

In the resulting Cypher, the Country node is bound to the variable $supplier_country.

Multiple Parallel Traversals

Pass multiple path arguments to traverse() to follow different relationship chains in a single query:

coffees = (
    Coffee.nodes
    .traverse("suppliers__country", "suppliers__delivery_cost")
    .all()
)

This executes one Cypher query that matches both paths simultaneously, reducing database round-trips.

Working with Subgraphs

When you need to navigate the returned data as a connected object graph rather than flat records, use resolve_subgraph():

results = Coffee.nodes.traverse("suppliers__country").resolve_subgraph().all()
first_coffee = results[0]

# Access related objects via _relations dictionary

suppliers = first_coffee._relations["suppliers"]
countries = first_coffee._relations["suppliers"][0]._relations["country"]

The resolve_subgraph method (defined at line 91 in neomodel/sync_/match.py) reconstructs the relationships between nodes that were returned as separate rows by the Cypher query.

Summary

  • Entry point: Call traverse() on any node set (Model.nodes.traverse(...)) to define relationship paths using double-underscore notation (e.g., "suppliers__country").
  • Core files: Implementation resides in neomodel/sync_/match.py (sync) and neomodel/async_/match.py (async), specifically the Path dataclass, BaseSet.traverse, and QueryBuilder.build_ast.
  • Customization: Use the Path dataclass to control return values (include_nodes_in_return, include_rels_in_return), make traversals optional (optional=True), or assign Cypher variable aliases.
  • Performance: Chain multiple paths in a single traverse() call to execute parallel matches in one Cypher query, minimizing database round-trips.
  • Object navigation: Apply resolve_subgraph() to convert flat query results into a nested Python object graph accessible via the _relations attribute.

Frequently Asked Questions

What is the difference between traverse() and fetch_relations()?

The fetch_relations() method is deprecated in favor of traverse(). While fetch_relations() provided basic relationship loading, traverse() offers a more powerful interface using the Path dataclass, supporting optional matches, custom return selectors, variable aliasing, and multi-path traversals in a single query.

How do I perform optional traversals that return nodes even when relationships don't exist?

Use the Path dataclass with optional=True. This generates an OPTIONAL MATCH clause in Cypher, equivalent to a SQL LEFT JOIN. For example: Path(value="suppliers__country", optional=True) ensures Coffee nodes are returned even if they have no suppliers or countries linked.

Can I traverse multiple relationship paths in a single database query?

Yes. The traverse() method accepts multiple positional arguments. Passing several path strings or Path objects—such as traverse("suppliers__country", "suppliers__delivery_cost")—executes both traversals in one Cypher query, reducing network overhead and ensuring consistent snapshot reads.

How do I access traversed relationships as Python objects instead of just node data?

Call resolve_subgraph() after traverse() and before executing the query (e.g., .traverse("suppliers__country").resolve_subgraph().all()). This method reconstructs the graph structure, exposing related nodes and relationships through the _relations dictionary on each returned node instance.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →