Skip to main content
For the complete documentation index optimized for AI agents, see llms.txt.
This guide is a tutorial walkthrough of the HelixDB query language. Each page builds on the last, starting from the simplest possible read and ending at parameter-bound bundles. Key examples show the TypeScript, Rust, Go, Python, and JSON forms across the guide, so you can pick a client and follow along or skim across the surfaces to see how they relate. These forms are not different query languages; they are encodings of the same query AST. The TypeScript, Rust, Go, and Python DSLs are builders that emit the JSON shape directly. If you copy the JSON column into a POST /v1/query body, it will execute the same query as the DSL above it.

Installing the SDKs

To follow along in TypeScript, Rust, Go, or Python, install the SDK for that language. All four emit the same query AST, so you can switch between them — or use the JSON column directly and skip the SDK entirely.
npm install @helix-db/helix-db
For the full project layout and generator setup, see TypeScript Project Setup, Rust Project Setup, Go Project Setup, and Python Project Setup.

The running example

Every page in this guide uses one small social-graph schema, so each new step plugs into a domain you already know.
  • Nodes: User (username, tier, createdAt), Post (title, body, embedding, createdAt), Tag (name).
  • Edges: FOLLOWS (User → User), AUTHORED (User → Post), LIKED (User → Post), TAGGED (Post → Tag).
  • Indexes: a unique-equality index on User.username, a vector index on Post.embedding, a text index on Post.body.
Index setup itself is covered on the Search page; for now assume they exist.

How a query is shaped

Every query — read or write — is a small pipeline:
  1. Open a batch with readBatch() or writeBatch().
  2. Bind one or more named sub-results with .varAs("name", traversal). A traversal always starts from g() and chains steps until it produces nodes, edges, or a terminal value.
  3. Choose which bound names to surface to the caller with .returning([...]).
The TypeScript shell:
readBatch()
  .varAs("name", g().someSource().someStep())
  .varAs("other", g().someSource().someStep())
  .returning(["name", "other"]);
The Rust shell is identical except for naming conventions (read_batch, var_as, returning). Go uses helix.ReadQuery("name"), VarAs, and variadic Returning. The JSON shell is what the batch serializes to: a top-level {queries: [...], returns: [...]} object, with each varAs producing one {Query: {name, steps, condition}} entry. The Reading nodes page covers the shell step by step; this overview just shows the finished product.

A full first example

Fetch a user by username and the ten most recent posts they authored. This example takes no parameters; it hard-codes "alice" and a limit of 10 so the JSON column is unambiguous. Parameter binding is introduced on the Filtering and Parameters & bundles pages.
import {
  g,
  NodeRef,
  Order,
  PropertyProjection,
  SourcePredicate,
  readBatch,
} from "@helix-db/helix-db";

const aliceFeed = readBatch()
  .varAs("user", g().nWhere(SourcePredicate.eq("username", "alice")))
  .varAs(
    "posts",
    g()
      .n(NodeRef.var("user"))
      .out("AUTHORED")
      .orderBy("createdAt", Order.Desc)
      .limit(10)
      .project([
        PropertyProjection.renamed("$id", "post_id"),
        PropertyProjection.new("title"),
        PropertyProjection.new("createdAt"),
      ]),
  )
  .returning(["user", "posts"]);

const request = aliceFeed.toDynamicRequest(); // POST this to /v1/query
Two things to notice:
  • The posts traversal starts from NodeRef.var("user") — that’s how one named binding feeds into the next. The same name appears as {"N": {"Var": "user"}} in the JSON.
  • The TypeScript, Rust, Go, and Python DSLs surface friendly nWithLabel / n_with_label / NWithLabel alias, but this example uses nWhere(SourcePredicate.eq("username", "alice")) directly because the read is anchored on a unique property, not a label scan. The next page covers when to use each anchor.

Sending the request

The JSON above is the actual body for POST /v1/query. Below are the same request in multiple transports — TypeScript, Rust, Go, and Python using their SDK clients, and curl straight from the shell. The transports are interchangeable; pick whichever fits your environment.
// Continuing from the snippet above.
import { Client } from "@helix-db/helix-db";

const client = new Client("https://helix.example.com");

const { user, posts } = await client
  .query<{ user: unknown; posts: unknown }>()
  .dynamic(request) // from .toDynamicRequest() above
  .send();
The response is a JSON object keyed by the names from .returning([...]) — here { "user": [...], "posts": [...] }. Each value is the list of rows produced by that named binding. The Parameters and bundles page shows how to bind parameters and organize queries into a queries.json bundle.

How to read this guide

Each page leads with a short intro, then <CodeGroup> blocks in all four forms. TypeScript is the lead tab; Rust mirrors it with snake_case names and trailing underscores for keywords (where_, in_, as_); Go mirrors the same AST via helix.ReadQuery / helix.WriteQuery; and JSON is the serialized AST you POST to /v1/query. Pages can be read in any order.

Next Steps

Reading nodes and edges

Source anchors: n by id, nWithLabel, nWhere, and the edge equivalents.

Traversals

Walking the graph: out, in, both, edge variants, multi-hop chains.

Filtering, ordering, paging

Predicates, ordering, and slicing the result stream.

Projections

Shaping the output: values, valueMap, project, aggregations.

Mutations

writeBatch, addN, addE, setProperty, drop.

Vector and text search

vectorSearchNodes, textSearchNodes, follow-on traversal from hits.

Advanced patterns

repeat, union, choose, coalesce, optional, varAsIf, forEachParam.

Parameters and bundles

defineParams, dynamic requests, bundles, typed call helpers.