# How to Use the Traversal API for Graph Traversals in neomodel

> Master neomodel graph traversals using the Traversal API. Build multi-hop Cypher queries efficiently with relationship paths and Path objects for powerful data exploration.

- Repository: [Neo4j Contrib/neomodel](https://github.com/neo4j-contrib/neomodel)
- Tags: how-to-guide
- Published: 2026-03-08

---

**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`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/match.py) (with an identical async counterpart in [`neomodel/async_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/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:

```python
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`:

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

```

This generates Cypher equivalent to:

```cypher
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:

```python
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()

```

```python

# 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:

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

```

Generated Cypher:

```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:

```python
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:

```python
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()`:

```python
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`](https://github.com/neo4j-contrib/neomodel/blob/main/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`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/match.py) (sync) and [`neomodel/async_/match.py`](https://github.com/neo4j-contrib/neomodel/blob/main/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.