Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.helix-db.com/llms.txt

Use this file to discover all available pages before exploring further.

For the complete documentation index optimized for AI agents, see llms.txt.
Every traversal in the DSL starts with g() — a fresh, empty graph cursor — followed by a source step that produces nodes or edges. This page covers the source steps: how to anchor on an id, a label, or an indexed property, and the difference between a SourcePredicate and a full Predicate.

The batch shell

Before any source step, you need a batch to hold the traversal. Reads use readBatch(); writes use writeBatch(). Inside, every named result is introduced with varAs("name", traversal), and returning([...]) chooses which names the caller receives.
import { g, readBatch } from "@helixdb/enterprise-ql";

const countUsers = readBatch()
  .varAs("user_count", g().nWithLabel("User").count())
  .returning(["user_count"]);
Two things worth noting from the JSON column:
  • nWithLabel("User") is a DSL convenience. On the wire it becomes a label-filtered source: {"NWhere": {"Eq": ["$label", {"String": "User"}]}}. The virtual $label field is implicit on every node.
  • count() is a unit step — it carries no payload — so it serializes as the bare string "Count" rather than a {Count: ...} object.

Anchoring on a node id

When you already have a node id (returned by an earlier query, or stored alongside your application data), pass it to g().n(...) to skip any index lookup at all.
import { g, NodeRef, readBatch } from "@helixdb/enterprise-ql";

const oneUser = readBatch()
  .varAs("user", g().n(NodeRef.id(42n)).valueMap(["$id", "username", "tier"]))
  .returning(["user"]);
NodeRef has several useful constructors:
  • NodeRef.id(42n) / NodeRef.ids([1n, 2n, 3n]) — one or many concrete ids.
  • NodeRef.var("name") — the result of an earlier varAs binding.
  • NodeRef.param("name") — a value supplied at request time (covered in Parameters & bundles).
  • NodeRef.all() — every node in the graph; rarely what you want.
Node ids fit in i64 and can exceed Number.MAX_SAFE_INTEGER. Use BigInt literals (42n) in TypeScript when authoring queries that touch ids directly.

Anchoring on a label

nWithLabel(label) selects every node of a given label. Without a follow-up filter this is a full label scan, so reach for it only when you genuinely want every node of that label.
import { g, readBatch } from "@helixdb/enterprise-ql";

const allTags = readBatch()
  .varAs("tags", g().nWithLabel("Tag").valueMap(["name"]))
  .returning(["tags"]);

Anchoring on a unique property

The most common starting shape: look up a node by an indexed property. Use nWhere(SourcePredicate.eq(...)) when there is no label scope, or nWithLabelWhere(label, SourcePredicate) when you want both at the source step.
import { g, readBatch, SourcePredicate } from "@helixdb/enterprise-ql";

const aliceByUsername = readBatch()
  .varAs(
    "user",
    g().nWhere(SourcePredicate.eq("username", "alice")),
  )
  .returning(["user"]);

SourcePredicate vs Predicate

Source steps (nWhere, eWhere, etc.) accept a SourcePredicate, which is a deliberately smaller set: eq, neq, gt, gte, lt, lte, between, hasKey, startsWith, plus and / or. These are the predicates the storage layer can push down into an index lookup. For richer post-filtering — collection membership, regex-ish contains/endsWith, isNull, not, parameter-bound comparisons — use a .where(Predicate.*) step after the source. That’s covered in Filtering. If you’ve already built a SourcePredicate and need to lift it into a full Predicate, every SourcePredicate exposes .toPredicate().

Anchoring on label plus a predicate

When the source step is both label-scoped and predicate-filtered, nWithLabelWhere collapses the two into one step. It produces the same JSON as nWhere(And[$label, ...]) but reads more clearly.
import { g, readBatch, SourcePredicate } from "@helixdb/enterprise-ql";

const proUsers = readBatch()
  .varAs(
    "pro_users",
    g().nWithLabelWhere("User", SourcePredicate.eq("tier", "pro")),
  )
  .returning(["pro_users"]);

Anchoring on edges

Edges have a parallel set of source steps:
  • g().e(EdgeRef.id(...)) / EdgeRef.ids([...]) — by id.
  • g().eWithLabel("FOLLOWS") — by edge label (e.g. every FOLLOWS edge in the graph).
  • g().eWhere(SourcePredicate.gt("weight", 0.5)) — by indexed edge property.
  • g().eWithLabelWhere("FOLLOWS", SourcePredicate.gte("since", "2026-01-01")) — label + predicate.
Edge streams support .outN() / .inN() / .otherN() to walk back to a node — see Traversals for the full edge story.
import { g, readBatch, SourcePredicate } from "@helixdb/enterprise-ql";

const recentFollows = readBatch()
  .varAs(
    "edges",
    g().eWithLabelWhere(
      "FOLLOWS",
      SourcePredicate.gte("since", "2026-01-01"),
    ),
  )
  .returning(["edges"]);

Picking the right anchor

Order of preference, narrowest first:
  1. Node or edge id (g().n(NodeRef.id(...))). No index lookup at all.
  2. Unique-indexed property (g().nWhere(SourcePredicate.eq("username", ...))).
  3. Equality-indexed property (same shape, non-unique index).
  4. Label-scoped predicate scan (g().nWithLabelWhere(...)).
  5. Plain label scan (g().nWithLabel(...)). Last resort.
If your route accepts an id or other indexed identifier from the request, use it as the anchor — never start from a broad label scan when an indexed identifier is available. Parameters & bundles shows the parameterized versions of every shape above.

Next Steps

Traversals

Walking the graph from your anchor: out, in, both, edge variants.

Filtering, ordering, paging

Predicates, .where, ordering, and .limit/.skip/.range.