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.
This page covers the steps that narrow a stream rather than walking the graph: property and label filters, the full Predicate catalog, ordering, and slicing the result with limit / skip / range.

.has, .hasLabel, .hasKey

The simplest filters are equality checks on a single property. They work on both node and edge streams.
import { g, readBatch } from "@helixdb/enterprise-ql";

const proUsers = readBatch()
  .varAs(
    "users",
    g().nWithLabel("User").has("tier", "pro").valueMap(["$id", "username"]),
  )
  .returning(["users"]);
  • .has(property, value) — property equals the given value.
  • .hasLabel(label) — node/edge label equals.
  • .hasKey(property) — property exists (any non-null value).
These are convenience wrappers; under the hood they could all be expressed via .where(...).

.where(Predicate.*)

For anything richer than equality, use .where(Predicate.*). Predicate exposes the full set:
GroupBuilders
Comparisoneq, neq, gt, gte, lt, lte, between
StringstartsWith, endsWith, contains
CollectionisIn, isInExpr
Property presencehasKey, isNull, isNotNull
Logicaland([...]), or([...]), not(p)
Parameter-boundeqParam, neqParam, gtParam, gteParam, ltParam, lteParam, isInParam, containsParam
Expressioncompare(left, op, right) — for arithmetic on either side
A typical multi-predicate filter:
import { g, Predicate, readBatch } from "@helixdb/enterprise-ql";

const recentProPosts = readBatch()
  .varAs(
    "posts",
    g()
      .nWithLabel("Post")
      .where(
        Predicate.and([
          Predicate.gte("createdAt", "2026-01-01"),
          Predicate.or([
            Predicate.contains("title", "graph"),
            Predicate.contains("title", "database"),
          ]),
          Predicate.isNotNull("body"),
        ]),
      )
      .valueMap(["$id", "title", "createdAt"]),
  )
  .returning(["posts"]);

SourcePredicate vs Predicate

Source steps (nWhere, eWhere, nWithLabelWhere, eWithLabelWhere) accept a SourcePredicate, which is deliberately smaller: comparison + between + hasKey
  • startsWith + and / or. These are the predicates the storage layer can push down into an index lookup.
Use a SourcePredicate when you want the filter to participate in index selection at the source step. Use .where(Predicate.*) when you need anything outside that set, or when you want the filter to run after a graph traversal step.

Parameter-bound predicates

To filter by a value the caller supplies at request time, use the *Param family. Pass the parameter name as a string; the runtime substitutes the value at execution. Full parameter declaration via defineParams is covered on Parameters & bundles; this page focuses on the predicate shape.
import {
  g,
  Predicate,
  defineParams,
  param,
  readBatch,
} from "@helixdb/enterprise-ql";

const params = defineParams({ username: param.string() });

function userByUsername(p = params) {
  return readBatch()
    .varAs(
      "user",
      g()
        .nWithLabel("User")
        .where(Predicate.eqParam("username", "username"))
        .valueMap(["$id", "username", "tier"]),
    )
    .returning(["user"]);
}

const body = userByUsername().toDynamicJson(params, { username: "alice" });
A few wire-format details worth noting:
  • Predicate.eqParam(prop, name) desugars to a generic Compare predicate with a Property on the left and a Param on the right. The same shape applies to neqParam, gtParam, gteParam, ltParam, lteParam (the op field varies).
  • Predicate.isInParam("status", "statuses") becomes {"IsInExpr": ["status", {"Param": "statuses"}]} — a Param-valued expression, not a Compare.
  • Predicate.containsParam("body", "needle") becomes {"ContainsExpr": ["body", {"Param": "needle"}]}.

Filtering edges in the middle of a traversal

When you’re walking through an edge stream and want to filter on edge properties without leaving the stream, use .edgeHas(prop, value) or .edgeHasLabel(label).
import { g, NodeRef, PropertyInput, readBatch, SourcePredicate } from "@helixdb/enterprise-ql";

const aliceStrongFollows = readBatch()
  .varAs("user", g().nWhere(SourcePredicate.eq("username", "alice")))
  .varAs(
    "strong_following",
    g()
      .n(NodeRef.var("user"))
      .outE("FOLLOWS")
      .edgeHas("weight", PropertyInput.value(0.8))
      .inN()
      .valueMap(["$id", "username"]),
  )
  .returning(["strong_following"]);

Ordering

.orderBy(property, Order.Asc | Order.Desc) sorts by a single property. .orderByMultiple([[prop, order], ...]) sorts by several at once with explicit direction per key.
import { g, Order, readBatch } from "@helixdb/enterprise-ql";

const recentPosts = readBatch()
  .varAs(
    "posts",
    g()
      .nWithLabel("Post")
      .orderBy("createdAt", Order.Desc)
      .limit(20)
      .valueMap(["$id", "title", "createdAt"]),
  )
  .returning(["posts"]);
For multi-key ordering:
g()
  .nWithLabel("Post")
  .orderByMultiple([
    ["createdAt", Order.Desc],
    ["title", Order.Asc],
  ]);
which serializes as:
{ "OrderByMultiple": [["createdAt", "Desc"], ["title", "Asc"]] }

Paging: .limit, .skip, .range

The three slicing steps all accept either a literal number / bigint or a StreamBound for parameter-bound bounds.
  • .limit(n) — keep the first n.
  • .skip(n) — drop the first n.
  • .range(start, end) — keep the half-open [start, end) slice.

Literal bounds

import { g, Order, readBatch } from "@helixdb/enterprise-ql";

const secondPage = readBatch()
  .varAs(
    "posts",
    g()
      .nWithLabel("Post")
      .orderBy("createdAt", Order.Desc)
      .range(20, 40)
      .valueMap(["$id", "title"]),
  )
  .returning(["posts"]);

Parameter-bound limits

For request-time pagination, wrap the bound in StreamBound.expr(Expr.param("...")). The JSON variant changes too: Limit becomes LimitBy, Skip becomes SkipBy, Range becomes RangeBy.
import {
  defineParams,
  Expr,
  g,
  Order,
  param,
  readBatch,
  StreamBound,
} from "@helixdb/enterprise-ql";

const params = defineParams({ page_size: param.i64() });

function recentPostsPaged(p = params) {
  return readBatch()
    .varAs(
      "posts",
      g()
        .nWithLabel("Post")
        .orderBy("createdAt", Order.Desc)
        .limit(StreamBound.expr(Expr.param("page_size")))
        .valueMap(["$id", "title"]),
    )
    .returning(["posts"]);
}

const body = recentPostsPaged().toDynamicJson(params, { page_size: 25n });
The Range parameter-bound variant looks like:
{ "RangeBy": [{ "Literal": 0 }, { "Expr": { "Param": "end" } }] }
— each bound is independently a Literal or an Expr, so you can mix-and-match a constant start with a parameterized end.

Next Steps

Projections

Choose what the caller sees: values, valueMap, project, terminal aggregations.

Mutations

addN, addE, setProperty, drop — write batches end-to-end.