Skip to main content

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.
Stored queries for HelixDB can also be authored in TypeScript using the @helixdb/enterprise-ql package. The TypeScript DSL builds the same JSON AST as the Rust helix-db crate, so the resulting queries.json bundle is interchangeable with one generated by Rust. Use TypeScript when your service is already a Node.js codebase, when you want end-to-end type inference on parameters from the same language that calls the routes, or when you prefer to keep query authoring close to the application that consumes them. For the conceptual model and per-builder walkthrough, see the Querying Guide.

Prerequisites

  • Node.js 20 or later.
  • A package manager (npm, pnpm, or yarn).
  • Optional: the Helix CLI for deploying the resulting bundle.

Create a new project

mkdir helix-queries
cd helix-queries
npm init -y
npm pkg set type=module
@helixdb/enterprise-ql is published as an ES module, so the "type": "module" field in package.json matters. Skip this step if you are adding queries to an existing ESM-configured project.

Add the dependency

npm install @helixdb/enterprise-ql
npm install --save-dev typescript @types/node tsx
tsx is optional but convenient for running the generator without a separate build step.

Minimal tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src/**/*.ts"]
}
The DSL relies on strict mode for the typestate-tracked traversal builder; turn it on.

Author your queries

Queries are plain functions that return a ReadBatch or WriteBatch. To turn them into a stored bundle, declare their parameters with defineParams, wrap them with registerRead / registerWrite, and bundle them with defineQueries. The bundle’s generate(path) method writes the same queries.json shape that the Rust generate_to_path emits.
src/queries.ts
import {
  NodeRef,
  Order,
  Predicate,
  PropertyInput,
  PropertyProjection,
  defineParams,
  defineQueries,
  g,
  param,
  readBatch,
  registerRead,
  registerWrite,
  writeBatch,
} from "@helixdb/enterprise-ql";

// Parameters for the read route.
const userByUsernameParams = defineParams({
  username: param.string(),
});

function userByUsername(p = userByUsernameParams) {
  return readBatch()
    .varAs(
      "user",
      g()
        .nWithLabel("User")
        .where(Predicate.eqParam("username", "username"))
        .project([
          PropertyProjection.renamed("$id", "id"),
          PropertyProjection.new("username"),
          PropertyProjection.new("tier"),
        ]),
    )
    .returning(["user"]);
}

// Parameters for the write route.
const addUserParams = defineParams({
  username: param.string(),
  tier: param.string(),
});

function addUser(p = addUserParams) {
  return writeBatch()
    .varAs(
      "newUser",
      g()
        .addN("User", {
          username: PropertyInput.param("username"),
          tier: PropertyInput.param("tier"),
        })
        .project([PropertyProjection.renamed("$id", "id")]),
    )
    .returning(["newUser"]);
}

export const queries = defineQueries({
  read: {
    user_by_username: registerRead(userByUsername, userByUsernameParams),
  },
  write: {
    add_user: registerWrite(addUser, addUserParams),
  },
});
A few things to know about this snippet:
  • Each query is a plain function. Calling the function returns a serializable ReadBatch or WriteBatch; nothing happens at module load.
  • The keys under read and write (user_by_username, add_user) become the route names. Once deployed, callers invoke POST /v1/query/user_by_username with a JSON body like {"username": "alice"}. Route names must be unique across reads and writes — a duplicate throws GenerateError at bundle time.
  • varAs("name", traversal) binds a sub-result, and .returning([...]) chooses which bound names appear in the response payload.
  • PropertyInput.param("name") is how a parameter feeds into an addN property bag or a setProperty call; for predicates use Predicate.eqParam(...) and friends.

Add a generator entry point

src/generate.ts
import { queries } from "./queries.js";

await queries.generate("queries.json");
console.log("generated queries.json");
queries.generate(path) writes the bundle and resolves to the path it wrote. The file is laid out identically to the Rust bundle so the runtime treats them as equivalent.

Wire up the script

{
  "scripts": {
    "generate": "tsx src/generate.ts"
  }
}
npm run generate
This produces queries.json in the project root. From here you can:

Calling registered routes from TypeScript

defineQueries also exposes a typed call map. Each entry takes the route’s parameter values (inferred from defineParams) and returns a serializable DynamicQueryRequest that you can POST directly:
import { queries } from "./queries.js";

const body = queries.call
  .user_by_username({ username: "alice" })
  .toJsonString();

await fetch("https://your-helix.example.com/v1/query/user_by_username", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body,
});
call.<route> is fully typed: passing an unknown key or the wrong value type for a parameter is a compile error.

Next Steps

Querying Guide

Tutorial-style walkthrough of the DSL, from the simplest read to parameter-bound stored bundles.

Working with HelixDB

Deploy queries.json and the runtime workflow.

Local Development

Run the bundle locally with the enterprise-dev image.

npm package

@helixdb/enterprise-ql on npm.