Skip to main content
For the complete documentation index optimized for AI agents, see llms.txt.
Helix Cloud exposes a single query surface over HTTP. Every query — whether a deep graph traversal, a vector search, a bulk write, or all three at once — is expressed in the same composable DSL and executes as one serializable transaction. Queries are authored once in TypeScript, Rust, Go, or Python (or by hand as JSON) and sent to the runtime as dynamic requests — the request body carries the query itself, so there is no separate deployment step. The Querying Guide walks the DSL end-to-end; this page covers how a dynamic query looks on the wire.

Installing the SDKs

The TypeScript, Rust, Go, and Python examples below are built with the Helix SDKs. Install the one for your language to follow along — all four emit the same dynamic query JSON.
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.

Dynamic queries

A dynamic query is sent inline: the request body carries the JSON AST for the query plus a small envelope that names the request kind, an optional query name, and any runtime parameters. There is no deployment step — the query travels with the request, so the same DSL that builds it also produces the body you POST. The example below counts every User node. The TypeScript, Rust, Go, and Python tabs are builder front-ends that produce the same JSON envelope; pick whichever your codebase already speaks. The JSON tab shows the resulting envelope verbatim — if you want to skip the DSL and POST hand-written JSON, this is the body you send.
import { g, readBatch } from "@helix-db/helix-db";

const countUsers = readBatch()
  .varAs("user_count", g().nWithLabel("User").count())
  .returning(["user_count"]);

// The request to POST to /v1/query.
const request = countUsers.toDynamicRequest({ queryName: "count_users" });
Sending that envelope is one POST to /v1/query. The response is a JSON object keyed by the names from .returning([...]). query_name is optional metadata for gateway logs and diagnostics. Use the exact field query_name (not name or queryName); it defaults to __dynamic__ when missing or null, and blank strings are rejected.
import { Client } from "@helix-db/helix-db";

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

// Builds the envelope and POSTs it to /v1/query.
const response = await client
  .query<{ user_count: number }>()
  .dynamic(request) // from .toDynamicRequest() above
  .send();

const { user_count } = response;
For larger envelopes, write the JSON to a file and use curl --data-binary @file.json — the inline --data-raw form gets unwieldy past a few steps. For parameters, bundles, and the typed queries.call.* helpers, see Parameters & bundles.

Running queries with the client

The SDKs ship a Client so you don’t have to assemble requests by hand. The client owns the connection details (URL and, on Helix Cloud, the API key) and posts dynamic requests to /v1/query.
import { Client, HelixError } from "@helix-db/helix-db";

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

try {
  const response = await client
    .query<{ user_count: number }>()
    .warmOnly() // X-Helix-Warm: true — pre-populate caches, discard the result
    .writerOnly() // X-Helix-Require-Writer: true — force the writer node
    .shouldAwaitDurability(true) // X-Helix-Await-Durable: true
    .dynamic(request)
    .send();
} catch (error) {
  if (error instanceof HelixError && error.kind === "Remote") {
    console.error(error.details); // server-side error body
  }
}

// Deployed named routes (POST /v1/query/{name}) use `.stored(name)`.
await client.query().body({ username: "alice" }).stored("user_by_username").send();
send() / Exec returns the parsed JSON response on HTTP 200. Any other status raises a HelixError whose kind is one of Network, Remote, Serialization, or InvalidUrl. In Go, remote errors are returned as *helix.HelixError with StatusCode populated; HTTP 409 Conflict also wraps helix.ErrConflict and can be checked with helix.IsConflict(err). The SDKs do not retry conflicts automatically, so retry only in application code when the operation is safe to replay.

Transactions

Every query — read or write — executes as a single transaction with serializable snapshot isolation.
  • Serializable. Transactions behave as if they executed one at a time, even when running concurrently.
  • Snapshot isolation. Each transaction reads from a consistent point-in-time snapshot. Reads within a transaction are never affected by concurrent writes.
  • Automatic. Transactions are implicit. Every query invocation is a transaction. There is no manual BEGIN / COMMIT / ROLLBACK.
Read-only queries never block writes. Write transactions are serialized through the single writer process for correctness without distributed locking. See Guarantees for the full consistency and durability contract.

Next Steps

Querying Guide

Tutorial walkthrough of the DSL with TypeScript, Rust, Go, Python, and JSON examples.

Parameters & bundles

defineParams, dynamic requests, bundles, and the typed call helpers in depth.

Working with HelixDB

Deploy-time and runtime workflow, query warming, and HTTP semantics.

Data Model

Nodes, edges, properties, and the labeled multigraph model.