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 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?
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.
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.
Surface different fields for free-tier vs paid-tier users:
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”.
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.
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
page, but it’s just as useful on the read side — fetch related rows only if the
primary lookup produced something.
condition field on the conditional Query entry carries the
constraint:
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.
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.
Next Steps
Parameters and bundles
defineParams, dynamic requests, stored bundles, typed call helpers.Querying overview
The conceptual story: stored vs dynamic queries, transactions, and how the bundle is deployed.