Skip to content

Traverse

Traversals let you navigate relationships in your graph. Instead of writing complex SQL joins, describe the path you want to follow.

Follow one edge from a node to connected nodes:

const employments = await store
.query()
.from("Person", "p")
.whereNode("p", (p) => p.id.eq("alice-123"))
.traverse("worksAt", "e") // Follow worksAt edges
.to("Company", "c") // Arrive at Company nodes
.select((ctx) => ({
person: ctx.p.name,
company: ctx.c.name,
role: ctx.e.role, // Edge properties are accessible
}))
.execute();
.traverse(edgeKind, edgeAlias, options?)
ParameterTypeDescription
edgeKindstringThe edge kind to traverse
edgeAliasstringUnique alias for referencing this edge
options.direction"out" | "in"Traversal direction (default: "out")
options.expand"none" | "implying" | "inverse" | "all"Ontology edge expansion mode (default: "inverse")
options.fromstringFan-out from a different node alias
.optionalTraverse(edgeKind, edgeAlias, options?)

Uses the same options as traverse(), but returns optional edge/node values in the result context.

.to(nodeKind, nodeAlias, options?)
ParameterTypeDescription
nodeKindstringThe target node kind
nodeAliasstringUnique alias for referencing this node
options.includeSubClassesbooleanInclude subclass kinds (default: false)

By default, traversals follow edges in their defined direction (from → to). Use direction: "in" to traverse backwards:

// Edge definition: worksAt goes from Person → Company
// Forward: Find companies where Alice works
.from("Person", "p")
.traverse("worksAt", "e") // Person → Company
.to("Company", "c")
// Backward: Find people who work at Acme
.from("Company", "c")
.whereNode("c", (c) => c.name.eq("Acme"))
.traverse("worksAt", "e", { direction: "in" }) // Company ← Person
.to("Person", "p")

Edges can carry properties. Access them through the edge alias:

const employments = await store
.query()
.from("Person", "p")
.traverse("worksAt", "e")
.to("Company", "c")
.select((ctx) => ({
person: ctx.p.name,
company: ctx.c.name,
role: ctx.e.role, // Edge property
salary: ctx.e.salary, // Edge property
startDate: ctx.e.startDate, // Edge property
}))
.execute();

Each edge provides these fields:

PropertyTypeDescription
idstringUnique edge identifier
kindstringEdge type name
fromIdstringID of the source node
toIdstringID of the target node
meta.createdAtstringWhen the edge was created
meta.updatedAtstringWhen the edge was last updated
meta.deletedAtstring | undefinedSoft delete timestamp
meta.validFromstring | undefinedTemporal validity start
meta.validTostring | undefinedTemporal validity end
schema propsvariesProperties defined in edge schema

Use whereEdge() to filter based on edge values:

const highPaying = await store
.query()
.from("Person", "p")
.traverse("worksAt", "e")
.whereEdge("e", (e) => e.salary.gte(100000))
.to("Company", "c")
.select((ctx) => ({
person: ctx.p.name,
company: ctx.c.name,
salary: ctx.e.salary,
}))
.execute();

Chain traversals to follow multiple relationships:

const projectTasks = await store
.query()
.from("Person", "person")
.whereNode("person", (p) => p.name.eq("Alice"))
.traverse("worksOn", "e1")
.to("Project", "project")
.traverse("hasTask", "e2")
.to("Task", "task")
.select((ctx) => ({
person: ctx.person.name,
project: ctx.project.name,
task: ctx.task.title,
}))
.execute();

Each hop starts from the previous node set and arrives at new nodes.

Combine forward and backward traversals:

const teamStructure = await store
.query()
.from("Person", "p")
.traverse("worksAt", "e1") // Forward: Person → Company
.to("Company", "c")
.traverse("manages", "e2", { direction: "in" }) // Backward: Person ← manages
.to("Person", "manager")
.select((ctx) => ({
employee: ctx.p.name,
company: ctx.c.name,
manager: ctx.manager.name,
}))
.execute();

Use optionalTraverse() for LEFT JOIN semantics—include results even when the traversal has no matches:

const peopleWithOptionalEmployer = await store
.query()
.from("Person", "p")
.optionalTraverse("worksAt", "e")
.to("Company", "c")
.select((ctx) => ({
person: ctx.p.name,
company: ctx.c?.name, // May be undefined if no employer
}))
.execute();
// Includes all people, even those without a worksAt edge
const employeesWithOptionalManager = await store
.query()
.from("Person", "p")
.traverse("worksAt", "e1") // Required: must work at a company
.to("Company", "c")
.optionalTraverse("reportsTo", "e2") // Optional: might not have manager
.to("Person", "manager")
.select((ctx) => ({
employee: ctx.p.name,
company: ctx.c.name,
manager: ctx.manager?.name, // undefined for top-level employees
}))
.execute();

With optional traversals, the edge may be undefined:

.select((ctx) => ({
person: ctx.p.name,
company: ctx.c?.name, // Node may be undefined
role: ctx.e?.role, // Edge may be undefined
salary: ctx.e?.salary,
}))

If your ontology defines edge implications, expand queries to include implying edges:

// Ontology: implies(marriedTo, knows), implies(bestFriends, knows)
const connections = await store
.query()
.from("Person", "p")
.whereNode("p", (p) => p.name.eq("Alice"))
.traverse("knows", "e", { expand: "implying" })
.to("Person", "other")
.select((ctx) => ctx.other.name)
.execute();
// Returns people connected via "knows", "marriedTo", or "bestFriends"

If your ontology defines inverse edge kinds, you can expand traversals to include inverse edges:

// Ontology: inverseOf(manages, managedBy)
const relationships = await store
.query()
.from("Person", "p")
.whereNode("p", (p) => p.name.eq("Alice"))
.traverse("manages", "e", { expand: "inverse" })
.to("Person", "other")
.select((ctx) => ({
name: ctx.other.name,
viaEdgeKind: ctx.e.kind,
}))
.execute();
// Traverses both "manages" and "managedBy"

You can combine both options:

.traverse("knows", "e", { expand: "all" })
const teamMembers = await store
.query()
.from("Person", "manager")
.whereNode("manager", (p) => p.name.eq("VP Engineering"))
.traverse("manages", "e")
.to("Person", "report")
.select((ctx) => ({
manager: ctx.manager.name,
report: ctx.report.name,
department: ctx.report.department,
}))
.execute();
const friends = await store
.query()
.from("Person", "me")
.whereNode("me", (p) => p.id.eq(currentUserId))
.traverse("follows", "e")
.to("Person", "friend")
.select((ctx) => ({
id: ctx.friend.id,
name: ctx.friend.name,
followedAt: ctx.e.createdAt,
}))
.orderBy("e", "createdAt", "desc")
.limit(50)
.execute();
const orderDetails = await store
.query()
.from("Order", "o")
.whereNode("o", (o) => o.id.eq(orderId))
.traverse("contains", "e")
.to("Product", "p")
.select((ctx) => ({
product: ctx.p.name,
quantity: ctx.e.quantity,
unitPrice: ctx.e.unitPrice,
}))
.execute();
  • Recursive - Variable-length paths with recursive()
  • Filter - Filter nodes and edges with predicates
  • Shape - Transform output with select()