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

# Advanced patterns

> For the complete documentation index optimized for AI agents, see [llms.txt](/llms.txt).

This page covers the steps that branch, repeat, or fan-out inside a single
traversal: `repeat`, `union`, `choose`, `coalesce`, `optional`, and — at the batch
level — `forEachParam`. All five sub-traversal-shaped steps share one helper:
`sub()`, which opens a fresh traversal builder that does not start with `g()`.

`sub()` is to in-line traversals what `g()` is to top-level ones. It has the same
chainable surface (`out`, `where`, `dedup`, etc.) but it is not a complete query on
its own — it is always passed into a parent step.

## `repeat`: variable-length traversal

`.repeat(RepeatConfig.new(sub()...).times(n))` walks the same sub-traversal `n`
times, threading the output of each iteration into the input of the next. By
default, only the final frontier is emitted. Add `.emitAll()` to keep every
intermediate frontier; add `.emitBefore()` / `.emitAfter()` to keep only one side.

A two-hop reachability example: from Alice, who is followed-of-followed-by within
three hops?

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

  const aliceReach = readBatch()
    .varAs("alice", g().nWhere(SourcePredicate.eq("username", "alice")))
    .varAs(
      "within_3",
      g()
        .n(NodeRef.var("alice"))
        .repeat(RepeatConfig.new(sub().out("FOLLOWS")).times(3).emitAll())
        .dedup()
        .valueMap(["$id", "username"]),
    )
    .returning(["within_3"]);
  ```

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

  #[register]
  pub fn alice_reach() -> ReadBatch {
      read_batch()
          .var_as("alice", g().n_where(SourcePredicate::eq("username", "alice")))
          .var_as(
              "within_3",
              g().n(NodeRef::var("alice"))
                  .repeat(
                      RepeatConfig::new(sub().out(Some("FOLLOWS")))
                          .times(3)
                          .emit_all(),
                  )
                  .dedup()
                  .value_map(Some(vec!["$id", "username"])),
          )
          .returning(["within_3"])
  }
  ```

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

  func AliceReach() helix.Request {
  	return helix.ReadQuery("alice_reach").
  		VarAs("alice", helix.G().NWhere(helix.SourceEq("username", "alice"))).
  		VarAs("within_3",
  			helix.G().
  				N(helix.NodeVar("alice")).
  				Repeat(helix.Repeat(helix.Sub().Out("FOLLOWS")).WithTimes(3).EmitAll()).
  				Dedup().
  				ValueMap("$id", "username"),
  		).
  		Returning("within_3")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "alice",
          "steps": [
            { "NWhere": { "Eq": ["username", { "String": "alice" }] } }
          ],
          "condition": null
        }
      },
      {
        "Query": {
          "name": "within_3",
          "steps": [
            { "N": { "Var": "alice" } },
            {
              "Repeat": {
                "traversal":       { "steps": [{ "Out": "FOLLOWS" }] },
                "times":           3,
                "until":           null,
                "emit":            "All",
                "emit_predicate":  null,
                "max_depth":       100
              }
            },
            "Dedup",
            { "ValueMap": ["$id", "username"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["within_3"]
  }
  ```
</CodeGroup>

The `max_depth: 100` field in the JSON is a safety ceiling — change it by chaining
`.maxDepth(n)` on the `RepeatConfig`. To stop early when a predicate is satisfied
instead of after `n` steps, swap `.times(3)` for `.until(predicate)`.

## `union`: combine multiple sub-traversals

`.union([sub()..., sub()...])` runs each sub-traversal from the current stream and
emits the concatenation. Use it when "the result is either A or B" — e.g. a personal
feed that mixes posts you authored *and* posts you liked.

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

  const feed = readBatch()
    .varAs("user", g().nWhere(SourcePredicate.eq("username", "alice")))
    .varAs(
      "posts",
      g()
        .n(NodeRef.var("user"))
        .union([sub().out("AUTHORED"), sub().out("LIKED")])
        .dedup()
        .valueMap(["$id", "title"]),
    )
    .returning(["posts"]);
  ```

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

  #[register]
  pub fn feed() -> ReadBatch {
      read_batch()
          .var_as("user", g().n_where(SourcePredicate::eq("username", "alice")))
          .var_as(
              "posts",
              g().n(NodeRef::var("user"))
                  .union(vec![sub().out(Some("AUTHORED")), sub().out(Some("LIKED"))])
                  .dedup()
                  .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 Feed() helix.Request {
  	return helix.ReadQuery("feed").
  		VarAs("user", helix.G().NWhere(helix.SourceEq("username", "alice"))).
  		VarAs("posts",
  			helix.G().
  				N(helix.NodeVar("user")).
  				Union(helix.Sub().Out("AUTHORED"), helix.Sub().Out("LIKED")).
  				Dedup().
  				ValueMap("$id", "title"),
  		).
  		Returning("posts")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "user",
          "steps": [
            { "NWhere": { "Eq": ["username", { "String": "alice" }] } }
          ],
          "condition": null
        }
      },
      {
        "Query": {
          "name": "posts",
          "steps": [
            { "N": { "Var": "user" } },
            {
              "Union": [
                { "steps": [{ "Out": "AUTHORED" }] },
                { "steps": [{ "Out": "LIKED" }] }
              ]
            },
            "Dedup",
            { "ValueMap": ["$id", "title"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["posts"]
  }
  ```
</CodeGroup>

## `choose`: per-item if/else

`.choose(predicate, thenSub, elseSub?)` evaluates the predicate per item and routes
each item through the matching sub-traversal. The optional `else` arm defaults to
"drop the item" — pass `null` (or omit it in Rust) for that.

Route paid-tier users through a premium feed edge and everyone else through the
default feed, then project the same fields from whichever branch matched:

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

  const recommendedPosts = readBatch()
    .varAs(
      "posts",
      g()
        .nWithLabel("User")
        .choose(
          Predicate.eq("tier", "pro"),
          sub().out("PRO_FEED"),
          sub().out("DEFAULT_FEED"),
        )
        .valueMap(["$id", "title"]),
    )
    .returning(["posts"]);
  ```

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

  #[register]
  pub fn recommended_posts() -> ReadBatch {
      read_batch()
          .var_as(
              "posts",
              g().n_with_label("User").choose(
                  Predicate::eq("tier", "pro"),
                  sub().out(Some("PRO_FEED")),
                  Some(sub().out(Some("DEFAULT_FEED"))),
              )
              .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 RecommendedPosts() helix.Request {
  	return helix.ReadQuery("recommended_posts").
  		VarAs("posts",
  			helix.G().
  				NWithLabel("User").
  				Choose(
  					helix.PredEq("tier", "pro"),
  					helix.Sub().Out("PRO_FEED"),
  					helix.Sub().Out("DEFAULT_FEED"),
  				).
  				ValueMap("$id", "title"),
  		).
  		Returning("posts")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "queries": [
      {
        "Query": {
          "name": "posts",
          "steps": [
            { "NWhere": { "Eq": ["$label", { "String": "User" }] } },
            {
              "Choose": {
                "condition":       { "Eq": ["tier", { "String": "pro" }] },
                "then_traversal":  { "steps": [{ "Out": "PRO_FEED" }] },
                "else_traversal":  { "steps": [{ "Out": "DEFAULT_FEED" }] }
              }
            },
            { "ValueMap": ["$id", "title"] }
          ],
          "condition": null
        }
      }
    ],
    "returns": ["posts"]
  }
  ```
</CodeGroup>

## `coalesce`: first non-empty branch wins

`.coalesce([sub()..., sub()..., sub()...])` tries each sub-traversal in order per
item and returns the first one that produces results. Useful for "prefer A, fall
back to B".

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
g()
  .n(NodeRef.var("user"))
  .coalesce([
    sub().out("PRIMARY_FEED"),
    sub().out("DEFAULT_FEED"),
  ])
```

The JSON form:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "Coalesce": [
    { "steps": [{ "Out": "PRIMARY_FEED" }] },
    { "steps": [{ "Out": "DEFAULT_FEED" }] }
  ]
}
```

## `optional`: keep the input even if the sub-traversal is empty

`.optional(sub())` runs the sub-traversal and lets each item through unchanged if the
sub-traversal produced no result. Think SQL `LEFT JOIN`: you get the original row
whether or not the optional walk matched anything.

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
g().nWithLabel("User").optional(sub().out("POSTED"));
```

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{ "Optional": { "steps": [{ "Out": "POSTED" }] } }
```

## Batch-level conditionals: `varAsIf`

Inside a `readBatch` / `writeBatch`, `.varAsIf(name, BatchCondition.*, traversal)`
makes a whole `varAs` step conditional on the result of an earlier one. This is the
upsert pattern from the [Mutations](/database/querying-guide/mutations#conditional-writes-with-varasif)
page, but it's just as useful on the read side — fetch related rows only if the
primary lookup produced something.

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
readBatch()
  .varAs("user", g().nWhere(SourcePredicate.eq("username", "alice")))
  .varAsIf(
    "posts",
    BatchCondition.varNotEmpty("user"),
    g().n(NodeRef.var("user")).out("AUTHORED").valueMap(["$id", "title"]),
  )
  .returning(["user", "posts"]);
```

In the JSON, the `condition` field on the conditional `Query` entry carries the
constraint:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "Query": {
    "name": "posts",
    "steps": [
      { "N":         { "Var": "user" } },
      { "Out":       "AUTHORED" },
      { "ValueMap": ["$id", "title"] }
    ],
    "condition": { "VarNotEmpty": "user" }
  }
}
```

## Fan-out over array parameters: `forEachParam`

`.forEachParam("paramName", body)` iterates over an array-of-object parameter and
runs `body` once per element. Inside `body`, each object's keys are addressable as
plain `PropertyInput.param(key)` references. This is the standard shape for
bulk-insert routes.

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

  const params = defineParams({
    users: param.array(param.object(param.string())),
  });

  function bulkAddUsers(p = params) {
    return writeBatch()
      .forEachParam(
        "users",
        writeBatch().varAs(
          "created",
          g().addN("User", {
            username: PropertyInput.param("username"),
            tier:     PropertyInput.param("tier"),
          }),
        ),
      )
      .returning(["created"]);
  }

  const body = bulkAddUsers().toDynamicJson(params, {
    users: [
      { username: "alice", tier: "pro" },
      { username: "bob",   tier: "free" },
    ],
  });
  ```

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

  #[register]
  pub fn bulk_add_users(users: ParamValue) -> WriteBatch {
      let _ = &users;
      write_batch()
          .for_each_param(
              "users",
              write_batch().var_as(
                  "created",
                  g().add_n(
                      "User",
                      vec![
                          ("username", PropertyInput::param("username")),
                          ("tier",     PropertyInput::param("tier")),
                      ],
                  ),
              ),
          )
          .returning(["created"])
  }
  ```

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

  func BulkAddUsers(users []map[string]any) helix.Request {
  	q := helix.WriteQuery("bulk_add_users")
  	q.ParamArray("users", users, helix.ParamTypeObject())

  	return q.
  		ForEachParam("users",
  			helix.Write().VarAs("created",
  				helix.G().AddN("User", helix.Props{
  					helix.Prop("username", helix.ParamInput("username")),
  					helix.Prop("tier", helix.ParamInput("tier")),
  				}),
  			),
  		).
  		Returning("created")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "request_type": "write",
    "query": {
      "queries": [
        {
          "ForEach": {
            "param": "users",
            "body": [
              {
                "Query": {
                  "name": "created",
                  "steps": [
                    {
                      "AddN": {
                        "label": "User",
                        "properties": [
                          ["username", { "Expr": { "Param": "username" } }],
                          ["tier",     { "Expr": { "Param": "tier" } }]
                        ]
                      }
                    }
                  ],
                  "condition": null
                }
              }
            ]
          }
        }
      ],
      "returns": ["created"]
    },
    "parameters": {
      "users": [
        { "username": "alice", "tier": "pro" },
        { "username": "bob",   "tier": "free" }
      ]
    },
    "parameter_types": {
      "users": { "Array": "Object" }
    }
  }
  ```
</CodeGroup>

A few notes specific to `forEachParam`:

* The body is a *whole* batch (`readBatch()` / `writeBatch()`), not a sub-traversal.
  This lets you bind multiple variables per iteration if needed.
* Bindings inside the loop body are *per-iteration*. The outer `returning([...])`
  receives the per-iteration results collected into one array.
* The parameter must be typed `{"Array": "Object"}` — an array of plain JSON
  objects. `defineParams({ users: param.array(param.object(...)) })` produces this
  shape automatically.
* Each loop iteration exposes the current object's top-level fields as scoped
  parameters. If a field itself is an object, pass it through as a whole property
  value, for example `PropertyInput.param("metadata")`, then query the stored
  nested fields later with dotted paths such as `metadata.externalID`.

## Correlate hops in one row: `bind` + `projectBindings`

A normal terminal (`.valueMap`, `.project`) only sees the **final** stream, so it
can't return a value captured earlier in the same traversal alongside a value
from a later hop. Row bindings solve this: tag elements with `.bind(name)` as the
traversal passes them, then assemble the output rows from those named bindings
with `.projectBindings([...])` (preserves duplicate rows) or
`.projectDistinctBindings([...])` (dedups identical rows).

`.bind()` does not change the stream — each path keeps its own row-local
bindings, so hops inside `union`, `optional`, and `choose` can still reference
captures made before the branch. Each `projection` reads from a binding (or the
current element) and emits one output column; `coalesce` takes the first present
non-null reference from a list.

<Note>
  Row bindings are available in the **TypeScript, Rust, and Go** SDKs. The Python
  SDK does not generate them yet — from Python, hand-write the `Bind` /
  `ProjectBindings` JSON AST shown in the JSON tab below.
</Note>

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

  function serviceTopology() {
    return readBatch()
      .varAs(
        "rows",
        g()
          .nWithLabel("Service")
          .bind("service")
          .out("ROUTES_TO").bind("pod")
          .optional(sub().in("CREATES").bind("deployment"))
          .union([
            sub().in("MANAGES").bind("owner"),
            sub().out("ROUTES_TO").bind("workload"),
          ])
          .projectDistinctBindings([
            BindingProjection.binding("service", "$id", "service_id"),
            BindingProjection.binding("pod", "name", "pod_name"),
            BindingProjection.coalesce([
              BindingProjection.bindingRef("deployment", "$id"),
              BindingProjection.bindingRef("owner", "$id"),
            ], "workload_id"),
          ]),
      )
      .returning(["rows"]);
  }
  ```

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

  #[register]
  pub fn service_topology() -> ReadBatch {
      read_batch()
          .var_as(
              "rows",
              g().n_with_label("Service")
                  .bind("service")
                  .out(Some("ROUTES_TO")).bind("pod")
                  .optional(sub().in_(Some("CREATES")).bind("deployment"))
                  .union(vec![
                      sub().in_(Some("MANAGES")).bind("owner"),
                      sub().out(Some("ROUTES_TO")).bind("workload"),
                  ])
                  .project_distinct_bindings(vec![
                      BindingProjection::binding("service", "$id", "service_id"),
                      BindingProjection::binding("pod", "name", "pod_name"),
                      BindingProjection::coalesce(
                          vec![
                              BindingValueRef::binding("deployment", "$id"),
                              BindingValueRef::binding("owner", "$id"),
                          ],
                          "workload_id",
                      ),
                  ]),
          )
          .returning(["rows"])
  }
  ```

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

  func ServiceTopology() helix.Request {
  	return helix.ReadQuery("service_topology").
  		VarAs("rows",
  			helix.G().
  				NWithLabel("Service").
  				Bind("service").
  				Out("ROUTES_TO").Bind("pod").
  				Optional(helix.Sub().In("CREATES").Bind("deployment")).
  				Union(
  					helix.Sub().In("MANAGES").Bind("owner"),
  					helix.Sub().Out("ROUTES_TO").Bind("workload"),
  				).
  				ProjectDistinctBindings(
  					helix.ProjectNamedBinding("service", "$id", "service_id"),
  					helix.ProjectNamedBinding("pod", "name", "pod_name"),
  					helix.ProjectBindingCoalesce([]helix.BindingValueRef{
  						helix.NamedBindingValue("deployment", "$id"),
  						helix.NamedBindingValue("owner", "$id"),
  					}, "workload_id"),
  				),
  		).
  		Returning("rows")
  }
  ```

  ```json JSON theme={"languages":{"custom":["languages/helixql.json"]}}
  {
    "request_type": "read",
    "query": {
      "queries": [
        {
          "Query": {
            "name": "rows",
            "steps": [
              { "NWhere": { "Eq": ["$label", { "String": "Service" }] } },
              { "Bind": "service" },
              { "Out": "ROUTES_TO" },
              { "Bind": "pod" },
              { "Optional": [{ "In": "CREATES" }, { "Bind": "deployment" }] },
              { "Union": [
                [{ "In": "MANAGES" }, { "Bind": "owner" }],
                [{ "Out": "ROUTES_TO" }, { "Bind": "workload" }]
              ] },
              { "ProjectBindings": {
                "projections": [
                  { "kind": "Property", "target": { "Binding": "service" }, "source": "$id", "alias": "service_id" },
                  { "kind": "Property", "target": { "Binding": "pod" }, "source": "name", "alias": "pod_name" },
                  { "kind": "Coalesce", "refs": [
                    { "target": { "Binding": "deployment" }, "source": "$id" },
                    { "target": { "Binding": "owner" }, "source": "$id" }
                  ], "alias": "workload_id" }
                ],
                "distinct": true
              } }
            ],
            "condition": null
          }
        }
      ],
      "returns": ["rows"]
    }
  }
  ```
</CodeGroup>

A few notes on row bindings:

* A `projection`'s `source` accepts stored properties and the virtual fields
  `$id`, `$label`, `$from`, `$to`, `$distance`, `$score` — the same set as
  `.project(...)`. The `target` is either a named binding or the current element
  (`BindingProjection.current(...)` in TS, `"Current"` on the wire).
* `coalesce` returns the first reference whose value is present and non-null,
  which is the idiomatic way to merge alternative branches of a `union` into one
  column.
* `projectBindings` keeps one row per surviving path including duplicates;
  `projectDistinctBindings` deduplicates identical projected rows. A wide
  `union` or `repeat` fan-out can multiply rows, so prefer the distinct form
  unless you need the duplicates.
* Binding queries serialize at query-bundle **v5**. SDKs still read older v4
  bundles, so this is backward-compatible for existing deployments.

## Next Steps

<CardGroup cols={2}>
  <Card title="Parameters and bundles" icon="gear" href="/database/querying-guide/parameters-bundles">
    `defineParams`, dynamic requests, bundles, typed call helpers.
  </Card>

  <Card title="Querying overview" icon="book-open" href="/database/querying">
    The conceptual story: dynamic queries, transactions, and the HTTP surface.
  </Card>
</CardGroup>
