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

# Filtering, ordering, paging

> For the complete documentation index optimized for AI agents, see [llms.txt](/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.

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import { g, readBatch } from "@helix-db/helix-db";

  const proUsers = readBatch()
    .varAs(
      "users",
      g().nWithLabel("User").has("tier", "pro").valueMap(["$id", "username"]),
    )
    .returning(["users"]);
  ```

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn pro_users() -> ReadBatch {
      read_batch()
          .var_as(
              "users",
              g().n_with_label("User")
                  .has("tier", "pro")
                  .value_map(Some(vec!["$id", "username"])),
          )
          .returning(["users"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func ProUsers() helix.Request {
  	return helix.ReadQuery("pro_users").
  		VarAs("users", helix.G().NWithLabel("User").Has("tier", "pro").ValueMap("$id", "username")).
  		Returning("users")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "users",
          "steps": [
            { "NWhere":   { "Eq": ["$label", { "String": "User" }] } },
            { "Has":      ["tier", { "String": "pro" }] },
            { "ValueMap": ["$id", "username"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["users"]
  }
  ```
</CodeGroup>

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

| Group             | Builders                                                                                          |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| Comparison        | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `between`                                                  |
| String            | `startsWith`, `endsWith`, `contains`                                                              |
| Collection        | `isIn`, `isInExpr`                                                                                |
| Property presence | `hasKey`, `isNull`, `isNotNull`                                                                   |
| Logical           | `and([...])`, `or([...])`, `not(p)`                                                               |
| Parameter-bound   | `eqParam`, `neqParam`, `gtParam`, `gteParam`, `ltParam`, `lteParam`, `isInParam`, `containsParam` |
| Expression        | `compare(left, op, right)` — for arithmetic on either side                                        |

A typical multi-predicate filter:

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import { g, Predicate, readBatch } from "@helix-db/helix-db";

  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"]);
  ```

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn recent_pro_posts() -> ReadBatch {
      read_batch()
          .var_as(
              "posts",
              g().n_with_label("Post")
                  .where_(Predicate::and(vec![
                      Predicate::gte("createdAt", "2026-01-01"),
                      Predicate::or(vec![
                          Predicate::contains("title", "graph"),
                          Predicate::contains("title", "database"),
                      ]),
                      Predicate::is_not_null("body"),
                  ]))
                  .value_map(Some(vec!["$id", "title", "createdAt"])),
          )
          .returning(["posts"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func RecentProPosts() helix.Request {
  	return helix.ReadQuery("recent_pro_posts").
  		VarAs("posts",
  			helix.G().
  				NWithLabel("Post").
  				Where(helix.PredAnd(
  					helix.PredGte("createdAt", "2026-01-01"),
  					helix.PredOr(
  						helix.PredContains("title", "graph"),
  						helix.PredContains("title", "database"),
  					),
  					helix.PredIsNotNull("body"),
  				)).
  				ValueMap("$id", "title", "createdAt"),
  		).
  		Returning("posts")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "posts",
          "steps": [
            { "NWhere": { "Eq": ["$label", { "String": "Post" }] } },
            {
              "Where": {
                "And": [
                  { "Gte": ["createdAt", { "String": "2026-01-01" }] },
                  {
                    "Or": [
                      { "Contains": ["title", "graph"] },
                      { "Contains": ["title", "database"] }
                    ]
                  },
                  { "IsNotNull": "body" }
                ]
              }
            },
            { "ValueMap": ["$id", "title", "createdAt"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["posts"]
  }
  ```
</CodeGroup>

### `SourcePredicate` vs `Predicate`

Use a `SourcePredicate` at source steps when the filter should participate in index
selection; use `.where(Predicate.*)` for anything outside that set or to filter after a
traversal step. See
[`SourcePredicate` vs `Predicate`](/database/querying-guide/reading-nodes#sourcepredicate-vs-predicate)
for the full breakdown.

## Nested property paths

Object properties can be filtered with dotted paths. The runtime first checks for
an exact top-level property name, then walks nested object fields when the name
contains `.`.

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import { g, Predicate, readBatch } from "@helix-db/helix-db";

  const usersByExternalId = readBatch()
    .varAs(
      "users",
      g()
        .nWithLabel("User")
        .where(Predicate.eq("metadata.externalID", "crm-42"))
        .valueMap(["$id", "username", "metadata.externalID"]),
    )
    .returning(["users"]);
  ```

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn users_by_external_id() -> ReadBatch {
      read_batch()
          .var_as(
              "users",
              g().n_with_label("User")
                  .where_(Predicate::eq("metadata.externalID", "crm-42"))
                  .value_map(Some(vec!["$id", "username", "metadata.externalID"])),
          )
          .returning(["users"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func UsersByExternalID() helix.Request {
  	return helix.ReadQuery("users_by_external_id").
  		VarAs("users",
  			helix.G().
  				NWithLabel("User").
  				Where(helix.PredEq("metadata.externalID", "crm-42")).
  				ValueMap("$id", "username", "metadata.externalID"),
  		).
  		Returning("users")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "users",
          "steps": [
            { "NWhere": { "Eq": ["$label", { "String": "User" }] } },
            { "Where": { "Eq": ["metadata.externalID", { "String": "crm-42" }] } },
            { "ValueMap": ["$id", "username", "metadata.externalID"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["users"]
  }
  ```
</CodeGroup>

Dotted paths are scan-only in V1. Pair them with an indexed top-level anchor such
as label, tenant, or status when the label is large; the indexed predicate narrows
candidate rows and the dotted predicate runs as a residual filter. Arrays are
opaque, so `metadata.tags.0` is not supported.

## 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](/database/querying-guide/parameters-bundles); this page focuses
on the predicate shape.

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import {
    g,
    Predicate,
    defineParams,
    param,
    readBatch,
  } from "@helix-db/helix-db";

  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" });
  ```

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn user_by_username(username: String) -> ReadBatch {
      let _ = &username;
      read_batch()
          .var_as(
              "user",
              g().n_with_label("User")
                  .where_(Predicate::eq_param("username", "username"))
                  .value_map(Some(vec!["$id", "username", "tier"])),
          )
          .returning(["user"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func UserByUsername(username string) helix.Request {
  	q := helix.ReadQuery("user_by_username")
  	usernameParam := q.ParamString("username", username)

  	return q.
  		VarAs("user",
  			helix.G().
  				NWithLabel("User").
  				Where(helix.PredEq("username", usernameParam)).
  				ValueMap("$id", "username", "tier"),
  		).
  		Returning("user")
  }

  body, err := helix.MarshalRequest(UserByUsername("alice"))
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "request_type": "read",
    "query": {
      "queries": [
        {
          "Query": {
            "name": "user",
            "steps": [
              { "NWhere": { "Eq": ["$label", { "String": "User" }] } },
              {
                "Where": {
                  "Compare": {
                    "left":  { "Property": "username" },
                    "op":    "Eq",
                    "right": { "Param": "username" }
                  }
                }
              },
              { "ValueMap": ["$id", "username", "tier"] }
            ],
            "condition": null
          }
        }
      ],
      "returns": ["user"]
    },
    "parameters":      { "username": "alice" },
    "parameter_types": { "username": "String" }
  }
  ```
</CodeGroup>

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 the same generic filters as node streams:
`.has(prop, value)`, `.hasLabel(label)`, `.hasKey(prop)`, and
`.where(Predicate.*)`.

Edge predicates can read stored edge properties plus the virtual edge fields
`$id`, `$label`, `$from`, `$to`, `$distance`, and `$score`. Use `.edgeHas(prop,
value)` when the right-hand side must be a `PropertyInput` expression or runtime
parameter.

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import { g, NodeRef, Predicate, readBatch, SourcePredicate } from "@helix-db/helix-db";

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

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn alice_strong_follows() -> ReadBatch {
      read_batch()
          .var_as("user", g().n_where(SourcePredicate::eq("username", "alice")))
          .var_as(
              "strong_following",
              g().n(NodeRef::var("user"))
                  .out_e(Some("FOLLOWS"))
                  .where_(Predicate::gte("weight", 0.8))
                  .in_n()
                  .value_map(Some(vec!["$id", "username"])),
          )
          .returning(["strong_following"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func AliceStrongFollows() helix.Request {
  	return helix.ReadQuery("alice_strong_follows").
  		VarAs("user", helix.G().NWhere(helix.SourceEq("username", "alice"))).
  		VarAs("strong_following",
  			helix.G().
  				N(helix.NodeVar("user")).
  				OutE("FOLLOWS").
  				Where(helix.PredGte("weight", helix.F64(0.8))).
  				InN().
  				ValueMap("$id", "username"),
  		).
  		Returning("strong_following")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "user",
          "steps": [
            { "NWhere": { "Eq": ["username", { "String": "alice" }] } }
          ],
          "condition": null
        }
      },
      {
        "Query": {
          "name": "strong_following",
          "steps": [
            { "N":           { "Var": "user" } },
            { "OutE":        "FOLLOWS" },
            { "Where":       { "Gte": ["weight", { "F64": 0.8 }] } },
            "InN",
            { "ValueMap":    ["$id", "username"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["strong_following"]
  }
  ```
</CodeGroup>

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

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import { g, Order, readBatch } from "@helix-db/helix-db";

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

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn recent_posts() -> ReadBatch {
      read_batch()
          .var_as(
              "posts",
              g().n_with_label("Post")
                  .order_by("createdAt", Order::Desc)
                  .limit(20usize)
                  .value_map(Some(vec!["$id", "title", "createdAt"])),
          )
          .returning(["posts"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func RecentPosts() helix.Request {
  	return helix.ReadQuery("recent_posts").
  		VarAs("posts",
  			helix.G().
  				NWithLabel("Post").
  				OrderBy("createdAt", helix.OrderDesc).
  				Limit(20).
  				ValueMap("$id", "title", "createdAt"),
  		).
  		Returning("posts")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "posts",
          "steps": [
            { "NWhere":   { "Eq": ["$label", { "String": "Post" }] } },
            { "OrderBy":  ["createdAt", "Desc"] },
            { "Limit":    20 },
            { "ValueMap": ["$id", "title", "createdAt"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["posts"]
  }
  ```
</CodeGroup>

For multi-key ordering:

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
g()
  .nWithLabel("Post")
  .orderByMultiple([
    ["createdAt", Order.Desc],
    ["title", Order.Asc],
  ]);
```

```go theme={"languages":{"custom":["languages/helixql.json"]}}
helix.G().NWithLabel("Post").OrderByMultiple(
	helix.Ordering{Property: "createdAt", Order: helix.OrderDesc},
	helix.Ordering{Property: "title", Order: helix.OrderAsc},
)
```

which serializes as:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{ "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

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import { g, Order, readBatch } from "@helix-db/helix-db";

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

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn second_page() -> ReadBatch {
      read_batch()
          .var_as(
              "posts",
              g().n_with_label("Post")
                  .order_by("createdAt", Order::Desc)
                  .range(20usize, 40usize)
                  .value_map(Some(vec!["$id", "title"])),
          )
          .returning(["posts"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func SecondPage() helix.Request {
  	return helix.ReadQuery("second_page").
  		VarAs("posts",
  			helix.G().
  				NWithLabel("Post").
  				OrderBy("createdAt", helix.OrderDesc).
  				Range(20, 40).
  				ValueMap("$id", "title"),
  		).
  		Returning("posts")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "posts",
          "steps": [
            { "NWhere":   { "Eq": ["$label", { "String": "Post" }] } },
            { "OrderBy":  ["createdAt", "Desc"] },
            { "Range":    [20, 40] },
            { "ValueMap": ["$id", "title"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["posts"]
  }
  ```
</CodeGroup>

### 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`.

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import {
    defineParams,
    Expr,
    g,
    Order,
    param,
    readBatch,
    StreamBound,
  } from "@helix-db/helix-db";

  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 });
  ```

  ```rust Rust theme={"languages":{"custom":["languages/helixql.json"]}}
  use helix_db::dsl::prelude::*;

  #[register]
  pub fn recent_posts_paged(page_size: i64) -> ReadBatch {
      let _ = &page_size;
      read_batch()
          .var_as(
              "posts",
              g().n_with_label("Post")
                  .order_by("createdAt", Order::Desc)
                  .limit(Expr::param("page_size"))
                  .value_map(Some(vec!["$id", "title"])),
          )
          .returning(["posts"])
  }
  ```

  ```go Go theme={"languages":{"custom":["languages/helixql.json"]}}
  import helix "github.com/helixdb/helix-db/sdks/go"

  func RecentPostsPaged(pageSize int64) helix.Request {
  	q := helix.ReadQuery("recent_posts_paged")
  	pageSizeParam := q.ParamI64("page_size", pageSize)

  	return q.
  		VarAs("posts",
  			helix.G().
  				NWithLabel("Post").
  				OrderBy("createdAt", helix.OrderDesc).
  				Limit(pageSizeParam).
  				ValueMap("$id", "title"),
  		).
  		Returning("posts")
  }

  body, err := helix.MarshalRequest(RecentPostsPaged(25))
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "request_type": "read",
    "query": {
      "queries": [
        {
          "Query": {
            "name": "posts",
            "steps": [
              { "NWhere":   { "Eq": ["$label", { "String": "Post" }] } },
              { "OrderBy":  ["createdAt", "Desc"] },
              { "LimitBy":  { "Param": "page_size" } },
              { "ValueMap": ["$id", "title"] }
            ],
            "condition": null
          }
        }
      ],
      "returns": ["posts"]
    },
    "parameters":      { "page_size": 25 },
    "parameter_types": { "page_size": "I64" }
  }
  ```
</CodeGroup>

The `Range` parameter-bound variant looks like:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{ "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

<CardGroup cols={2}>
  <Card title="Projections" icon="table-columns" href="/database/querying-guide/projections">
    Choose what the caller sees: `values`, `valueMap`, `project`, terminal aggregations.
  </Card>

  <Card title="Mutations" icon="pen-to-square" href="/database/querying-guide/mutations">
    `addN`, `addE`, `setProperty`, `drop` — write batches end-to-end.
  </Card>
</CardGroup>
