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

# Parameters and bundles

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

So far every parameterized example on the prior pages has shown
`p = paramsObject` / `Predicate.eqParam("prop", "name")` / `toDynamicJson(params, values, options)`
without dwelling on the parameter system itself. This page is the dedicated treatment:
how to declare parameters, how to refer to them in traversals, and how to ship a
parameterized query to the runtime.

## SDK workflows

TypeScript, Rust, and Python support both direct dynamic requests and registered bundle
workflows. Go v1 is dynamic-first: write normal Go functions that return
`helix.Request`, set the query name with `helix.ReadQuery("name")` or
`helix.WriteQuery("name")`, and declare runtime parameters inline on the query
builder.

```go theme={"languages":{"custom":["languages/helixql.json"]}}
func FindUsers(tenantID string, limit int64) helix.Request {
	q := helix.ReadQuery("find_users")

	tenant := q.ParamString("tenant_id", tenantID)
	maxRows := q.ParamI64("limit", limit)

	return q.
		VarAs("users",
			helix.G().
				NWithLabel("User").
				Where(helix.PredEq("tenantId", tenant)).
				Limit(maxRows).
				ValueMap("$id", "name", "tenantId"),
		).
		Returning("users")
}

var out FindUsersResponse
err := client.Exec(ctx, FindUsers("acme", 25), &out)
```

The Go helpers insert both `parameters` and `parameter_types` in the dynamic
envelope. Use `q.ParamString`, `q.ParamI64`, `q.ParamF64`, `q.ParamF32`,
`q.ParamBool`, `q.ParamDateTime`, `q.ParamValue`, `q.ParamObject`, and
`q.ParamArray`. Do not use `.With(...)`, `WithQueryName(...)`, or stored-query
bundle APIs in the Go v1 workflow. Use `MarshalRequest(req)` only for tests,
parity fixtures, or debugging.

In Go, direct values are literals in the inline AST. `helix.SourceEq("id", "foo")`
and `helix.PredEq("id", "foo")` embed `"foo"` directly; they do not create a
runtime parameter. For values that change per request, declare a `q.Param*` value
and pass the returned ref, for example `id := q.ParamString("id", userID)` and
`helix.SourceEq("id", id)`. That keeps the request shape stable for server-cache
reuse.

Python mirrors the TypeScript/Rust direct dynamic and bundle workflow with snake\_case helpers:

```python theme={"languages":{"custom":["languages/helixql.json"]}}
from helixdb import Predicate, define_params, g, param, read_batch

params = define_params({"tenant_id": param.string(), "limit": param.i64()})

def find_users(p=params):
    return (
        read_batch()
        .var_as(
            "users",
            g()
            .n_with_label("User")
            .where(Predicate.eq("tenantId", p.tenant_id))
            .limit(p.limit)
            .value_map(["$id", "name", "tenantId"]),
        )
        .returning(["users"])
    )

request = find_users().to_dynamic_request(params, {"tenant_id": "acme", "limit": 25}, query_name="find_users")
```

## Declaring parameters with `defineParams` / `define_params`

This section applies to TypeScript and Python bundle and dynamic helpers. Rust derives a
similar schema from registered function arguments. Go declares the same schema
inline through `q.Param*` methods as shown above.

`defineParams({...})` in TypeScript and `define_params({...})` in Python take a record
of parameter names to `ParamSchema` constructors and return a proxy. Each property
of the returned proxy is a `ParamRef` you can pass into builder methods that expect one.

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

const params = defineParams({
  tenant_id:     param.string(),
  username:      param.string(),
  limit:         param.i64(),
  query_vector:  param.array(param.f32()),
  created_after: param.dateTime(),
});
```

```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
from helixdb import define_params, param

params = define_params({
    "tenant_id": param.string(),
    "username": param.string(),
    "limit": param.i64(),
    "query_vector": param.array(param.f32()),
    "created_after": param.date_time(),
})
```

The supported constructors share the same wire types; Python uses snake\_case for the datetime helper:

| TypeScript                    | Python                        | Wire type        | Accepted dynamic values                                                                      |
| ----------------------------- | ----------------------------- | ---------------- | -------------------------------------------------------------------------------------------- |
| `param.bool()`                | `param.bool()`                | `Bool`           | `boolean` / `bool`                                                                           |
| `param.i64()`                 | `param.i64()`                 | `I64`            | safe integer, `bigint`, or Python `int`                                                      |
| `param.f64()` / `param.f32()` | `param.f64()` / `param.f32()` | `F64` / `F32`    | finite number / `float`                                                                      |
| `param.string()`              | `param.string()`              | `String`         | `string` / `str`                                                                             |
| `param.dateTime()`            | `param.date_time()`           | `DateTime`       | `DateTime`, RFC3339 string, or epoch-millis integer; Python also accepts `datetime.datetime` |
| `param.bytes()`               | `param.bytes()`               | `Bytes`          | declarable, but dynamic JSON requests reject bytes values                                    |
| `param.value()`               | `param.value()`               | `Value`          | any JSON value                                                                               |
| `param.object(inner?)`        | `param.object(inner=None)`    | `Object`         | object values matching the optional inner schema                                             |
| `param.array(inner)`          | `param.array(inner)`          | `{Array: inner}` | arrays/lists matching the inner schema                                                       |

`param.array(param.f32())` is what produces the `{"Array": "F32"}` shape required for
vector parameters. Use `param.array(param.object(...))` for row-style inputs to
`forEachParam` / `for_each_param`.

## Referring to parameters inside a traversal

There are three ways a parameter shows up inside a query, depending on what kind of
value the builder expects:

1. **Bound to a step argument that takes a `ParamRef`** - for `.limit(p.limit)`,
   `.skip(p.skip)`, etc. The proxy returned by `defineParams` / `define_params` is
   the value to pass.
2. **Inside a predicate** - TypeScript offers `Predicate.eqParam("prop", "param_name")`
   and the rest of the `*Param` family for string-keyed parameter refs. Python can
   pass the `ParamRef` directly, for example `Predicate.eq("tenantId", p.tenant_id)`.
3. **As a property value or expression** - `PropertyInput.param("name")`,
   `Expr.param("name")`, `NodeRef.param("name")`, and `EdgeRef.param("name")` all take
   the name as a string. In Python, `p.tenant_id.input()` is a shorthand when a method
   expects a `PropertyInput`.

In practice the proxy is mostly used to keep parameter names in sync - if you
rename the field in `defineParams` / `define_params`, every reference through the
proxy moves with it. For string-keyed refs (`PropertyInput.param("name")`), the name
has to match exactly. A common convention is to assign the proxy to a `p` argument
and read both forms from there:

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  function findUsers(p = params) {
    return readBatch().varAs(
      "users",
      g()
        .nWithLabel("User")
        .where(Predicate.eqParam("tenantId", "tenant_id"))   // string name
        .limit(p.limit)                                       // proxy ref
        .valueMap(["$id", "username", "tenantId"]),
    ).returning(["users"]);
  }
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  def find_users(p=params):
      return (
          read_batch()
          .var_as(
              "users",
              g()
              .n_with_label("User")
              .where(Predicate.eq("tenantId", p.tenant_id))  # proxy ref
              .limit(p.limit)
              .value_map(["$id", "username", "tenantId"]),
          )
          .returning(["users"])
      )
  ```
</CodeGroup>

## Shipping a parameterized query

A query built with `readBatch()` / `writeBatch()` is sent to the runtime as a
dynamic request. There are a few ways to produce that request — straight from a
batch, from a registered bundle, or through the typed `call` helpers — but they
all serialize to the same dynamic envelope you POST to `/v1/query`.

### 1. From a batch directly

The simplest case: build the batch, call `.toDynamicJson(params, values, options)`
in TypeScript or `.to_dynamic_json(params, values, query_name=...)` in Python, then
POST the resulting body to `/v1/query`. No deploy step. Best for ad-hoc queries,
prototyping, and one-off integrations.

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
import { defineParams, g, param, Predicate, readBatch } from "@helix-db/helix-db";

const params = defineParams({ username: param.string() });

function userByUsername(p = params) {
  return readBatch()
    .varAs(
      "user",
      g().nWithLabel("User")
         .where(Predicate.eqParam("username", "username"))
         .valueMap(["$id", "username", "tier"]),
    )
    .returning(["user"]);
}
```

The dynamic envelope produced by `.toDynamicJson(...)` / `.to_dynamic_json(...)` is
the body for `POST /v1/query`. Below are the five equivalent ways to send it:

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

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

  const request = userByUsername().toDynamicRequest(
    params,
    { username: "alice" },
    { queryName: "user_by_username" },
  );

  const { user } = await client
    .query<{ user: unknown }>()
    .dynamic(request)
    .send();
  ```

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

  let client = Client::new(Some("https://helix.example.com"))?;

  let response: serde_json::Value = client
      .query()
      .dynamic(user_by_username("alice".to_string()))
      .send()
      .await?;

  let user = &response["user"];
  ```

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

  func UserByUsername(username string) helix.Request {
  	q := helix.ReadQuery("user_by_username")
  	usernameParam := q.ParamString("username", username)

  	return q.
  		VarAs("user",
  			helix.G().
  				NWithLabel("User").
  				Where(helix.PredEq("username", usernameParam)).
  				ValueMap("$id", "username", "tier"),
  		).
  		Returning("user")
  }

  client, err := helix.NewClient("https://helix.example.com")
  if err != nil {
  	return err
  }

  var response struct {
  	User []map[string]any `json:"user"`
  }
  if err := client.Exec(ctx, UserByUsername("alice"), &response); err != nil {
  	return err
  }

  user := response.User
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  from helixdb import Client, Predicate, define_params, g, param, read_batch

  params = define_params({"username": param.string()})

  def user_by_username(p=params):
      return (
          read_batch()
          .var_as(
              "user",
              g()
              .n_with_label("User")
              .where(Predicate.eq("username", p.username))
              .value_map(["$id", "username", "tier"]),
          )
          .returning(["user"])
      )

  client = Client("https://helix.example.com")
  request = user_by_username().to_dynamic_request(params, {"username": "alice"}, query_name="user_by_username")
  user = client.query().dynamic(request).send()["user"]
  ```

  ```bash curl theme={"languages":{"custom":["languages/helixql.json"]}}
  curl -sS -X POST https://helix.example.com/v1/query \
    -H "content-type: application/json" \
    --data-raw '{
      "request_type": "read",
      "query_name": "user_by_username",
      "query": {
        "queries": [
          { "Query": { "name": "user", "steps": [
            { "NWhere": { "Eq": ["$label", { "String": "User" }] } },
            { "Where": { "Compare": {
              "left":  { "Property": "username" },
              "op":    "Eq",
              "right": { "Param": "username" }
            } } },
            { "ValueMap": ["$id", "username", "tier"] }
          ], "condition": null } }
        ],
        "returns": ["user"]
      },
      "parameters":      { "username": "alice" },
      "parameter_types": { "username": "String" }
    }'
  ```
</CodeGroup>

For larger envelopes, write the JSON to a file and use `--data-binary @file.json` —
the inline `--data-raw` form works for small bodies but gets unwieldy fast.

The three serialization helpers on a batch:

| TypeScript                                      | Python                                                          | Returns                                                                                             |
| ----------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `.toDynamicJson(params?, values?, options?)`    | `.to_dynamic_json(params=None, values=None, query_name=...)`    | JSON string (full envelope: `request_type`, `query_name`, `query`, `parameters`, `parameter_types`) |
| `.toDynamicRequest(params?, values?, options?)` | `.to_dynamic_request(params=None, values=None, query_name=...)` | `DynamicQueryRequest` instance - useful when you want to mutate it before serializing               |
| `.toDynamicBytes(params?, values?, options?)`   | `.to_dynamic_bytes(params=None, values=None, query_name=...)`   | UTF-8 bytes of the JSON encoding                                                                    |

Pass `{ queryName: "..." }` as the final TypeScript options argument, or
`query_name="..."` in Python, when you want gateway logs and query diagnostics to
identify a direct inline request. Direct requests without a name serialize
`query_name: null`, which the gateway records as the fallback dynamic name
`__dynamic__`.

For a parameter-less batch, omit `params` and `values`; pass only the name option
if you want to name the request:

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  readBatch().varAs("count", g().nWithLabel("User").count()).toDynamicJson({
    queryName: "count_users",
  });
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  read_batch().var_as("count", g().n_with_label("User").count()).to_dynamic_json(
      query_name="count_users",
  )
  ```
</CodeGroup>

`.toJsonString()` / `.to_json_string()` returns the *raw batch* (no envelope, no
parameters), the form that goes into a bundle - covered below.

### 2. Registered bundles

Registered bundles are available in TypeScript, Rust, and Python. Go v1 does not generate
or register bundles; keep Go application queries dynamic-first with
`client.Exec(ctx, request, &out)`.

To keep a set of queries organized and generate a single `queries.json` artifact,
wrap each query function with `registerRead` or `registerWrite` and pass them all to
`defineQueries`. The bundle's `generate(path)` method writes a single `queries.json`
in the exact shape the runtime expects.

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

  const readParams = defineParams({ username: param.string() });

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

  const writeParams = defineParams({
    username: param.string(),
    tier:     param.string(),
  });

  function addUser(p = writeParams) {
    return writeBatch()
      .varAs(
        "newUser",
        g().addN("User", {
          username: PropertyInput.param("username"),
          tier:     PropertyInput.param("tier"),
        }),
      )
      .returning(["newUser"]);
  }

  export const queries = defineQueries({
    read:  { user_by_username: registerRead(userByUsername, readParams) },
    write: { add_user:         registerWrite(addUser, writeParams) },
  });

  // Build queries.json at deploy time.
  await queries.generate("queries.json");
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  from helixdb import (
      Predicate,
      PropertyInput,
      define_params,
      define_queries,
      g,
      param,
      read_batch,
      register_read,
      register_write,
      write_batch,
  )

  read_params = define_params({"username": param.string()})


  def user_by_username(p=read_params):
      return (
          read_batch()
          .var_as(
              "user",
              g()
              .n_with_label("User")
              .where(Predicate.eq("username", p.username))
              .value_map(["$id", "username", "tier"]),
          )
          .returning(["user"])
      )

  write_params = define_params({"username": param.string(), "tier": param.string()})


  def add_user(p=write_params):
      return (
          write_batch()
          .var_as(
              "newUser",
              g().add_n("User", {
                  "username": PropertyInput.param("username"),
                  "tier": PropertyInput.param("tier"),
              }),
          )
          .returning(["newUser"])
      )

  queries = define_queries({
      "read": {"user_by_username": register_read(user_by_username, read_params)},
      "write": {"add_user": register_write(add_user, write_params)},
  })

  # Build queries.json at deploy time.
  queries.generate("queries.json")
  ```
</CodeGroup>

The resulting bundle has this top-level shape:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "version": 5,
  "read_routes":  { "user_by_username": { "queries": [...], "returns": ["user"] } },
  "write_routes": { "add_user":         { "queries": [...], "returns": ["newUser"] } },
  "read_parameters": {
    "user_by_username": [{ "name": "username", "ty": "String" }]
  },
  "write_parameters": {
    "add_user": [
      { "name": "username", "ty": "String" },
      { "name": "tier",     "ty": "String" }
    ]
  }
}
```

Each `read_routes.<name>` / `write_routes.<name>` is exactly what `.toJsonString()`
would have produced for the underlying batch — no envelope. To execute one of these
queries, produce its dynamic envelope through the typed `call` helpers below and POST
it to `/v1/query`.

Route names must be unique across reads *and* writes; duplicates throw `GenerateError`
at bundle time.

### 3. Typed call helpers

`defineQueries(...)` / `define_queries(...)` also exposes a `call` map. Each entry
is a function that takes the route's parameter values and returns a serializable
`DynamicQueryRequest`. TypeScript infers the input type from `defineParams`; Python
validates unknown keys, missing keys, and wrong value types at call time.

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  queries.call.user_by_username({
    username: "alice",
    unexpected: true,  // ts(...): Object literal may only specify known properties
  });

  queries.call.user_by_username({
    username: 1,  // ts(...): Type 'number' is not assignable to type 'string'
  });
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  queries.call.user_by_username({"username": "alice"})

  # Raises TypeError: unknown parameter: unexpected
  queries.call.user_by_username({"username": "alice", "unexpected": True})

  # Raises TypeError during parameter conversion.
  queries.call.user_by_username({"username": 1})
  ```
</CodeGroup>

The returned object serializes to the same dynamic envelope you would build by hand,
so you POST it to `/v1/query`:

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

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

  const { user } = await client
    .query<{ user: unknown }>()
    .dynamic(queries.call.user_by_username({ username: "alice" }))
    .send();
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  from helixdb import Client

  client = Client("https://helix.example.com")
  user = client.query().dynamic(queries.call.user_by_username({"username": "alice"})).send()["user"]
  ```
</CodeGroup>

`queries.call.*` keeps parameter names and types in sync with `defineParams` /
`define_params`, so it is the most convenient way to send a registered query - in
application code, tests, and scripts alike. The returned dynamic request sets
`query_name` to the route key (`user_by_username` in this example) automatically.

## How parameters serialize

Inside the AST, parameter references appear as `{"Param": "name"}` wrapped in the
spot the value would go: `{"Limit": ... }` becomes `{"LimitBy": {"Param": "n"}}`,
`{"Has": ["k", value]}` becomes `{"Where": {"Compare": {"left": {"Property": "k"},
"op": "Eq", "right": {"Param": "k"}}}}`, and so on. The
[Filtering](/database/querying-guide/filtering) page tabulates every shape.

At envelope position, the actual value is a *bare* JSON value, *not* a tagged
`PropertyValue`. The shape `{"Array": "F32"}` you see in the `parameter_types` map
is `QueryParamType`, the runtime coercion schema.

A complete envelope produced by `toDynamicJson` / `to_dynamic_json`:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "request_type": "read",
  "query_name": "user_by_username",
  "query": { "queries": [...], "returns": [...] },
  "parameters": {
    "username":      "alice",
    "limit":         25,
    "created_after": "2026-04-05T10:34:56.789Z",
    "labels":        { "status": "active" }
  },
  "parameter_types": {
    "username":      "String",
    "limit":         "I64",
    "created_after": "DateTime",
    "labels":        "Object"
  }
}
```

Nested object and array parameter values are still bare JSON at envelope
position. When a parameter is used as a property value, the runtime converts that
JSON object into a stored object property:

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
const params = defineParams({ metadata: param.object(param.value()) });

writeBatch().varAs(
  "user",
  g().addN("User", { metadata: PropertyInput.param("metadata") }),
);
```

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "parameters": {
    "metadata": {
      "externalID": "crm-42",
      "preferences": { "locale": "en-US" },
      "tags": ["trial", 7]
    }
  },
  "parameter_types": { "metadata": "Object" }
}
```

After storage, query nested object fields with dotted paths such as
`metadata.externalID`. The top-level `parameters` map itself does not use tagged
`PropertyValue` wrappers.

## Number handling: `bigint` for `i64`

JavaScript `number` only has 53 bits of integer precision. The TypeScript DSL accepts
a plain `number` wherever it fits in a safe integer range, but for full `i64` (node
ids, large counts, epoch nanos) you should pass a `bigint`:

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
g().n(9223372036854775807n);
PropertyValue.i64(9223372036854775807n);
```

Python `int` values are arbitrary precision, so the Python SDK validates the value
against the `i64` range and serializes it directly.

`stringifyJson`, `.toJsonString()` / `.to_json_string()`, and `serializeQueryBundle`
preserve large integers without loss. Plain `JSON.stringify` does not - use the
package serializers when your TypeScript payload may contain large integers.

## DateTime handling

`DateTime` is the dedicated value type for timestamps. It stores epoch milliseconds
and round-trips losslessly through the dynamic envelope as a UTC RFC3339 string.

```ts theme={"languages":{"custom":["languages/helixql.json"]}}
import { DateTime } from "@helix-db/helix-db";

DateTime.fromMillis(-1).toRfc3339();
// "1969-12-31T23:59:59.999Z"

DateTime.parseRfc3339("2026-04-05T12:34:56.789+02:00").toRfc3339();
// "2026-04-05T10:34:56.789Z"  (normalized to UTC)
```

When a parameter is declared as `param.dateTime()` in TypeScript or
`param.date_time()` in Python, the call helpers accept `DateTime`, an RFC3339
string, or an epoch-millis integer. Python also accepts `datetime.datetime`:

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  queries.call.recent_posts({
    since: DateTime.fromMillis(0),
  });
  queries.call.recent_posts({
    since: "2026-01-01T00:00:00Z",  // auto-parsed
  });
  queries.call.recent_posts({
    since: 1735689600000n,
  });
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  from datetime import datetime, timezone
  from helixdb import DateTime

  queries.call.recent_posts({"since": DateTime.from_millis(0)})
  queries.call.recent_posts({"since": "2026-01-01T00:00:00Z"})
  queries.call.recent_posts({"since": datetime(2026, 1, 1, tzinfo=timezone.utc)})
  ```
</CodeGroup>

All of these values serialize to the same envelope:

```json theme={"languages":{"custom":["languages/helixql.json"]}}
{
  "parameters":      { "since": "2026-01-01T00:00:00.000Z" },
  "parameter_types": { "since": "DateTime" }
}
```

## Bytes parameters

`param.bytes()` is declarable in `defineParams` / `define_params`, but the dynamic
envelope cannot round-trip binary values faithfully. Attempting to call
`toDynamicJson(params, { payload: someBytes })`, `to_dynamic_json(params, {"payload": b"..."})`,
or to build a request with a bytes parameter through `queries.call.*` throws an
unsupported-bytes dynamic query error.

If you need to pass binary data, send it through a binary-aware client (the runtime's
RPC interface) rather than the JSON dynamic envelope, which is the bottleneck.

## Generating the bundle from a script

The `queries.generate(path)` shortcut is enough for most projects. If you want to
inspect or post-process the bundle first, the lower-level helpers are exported too:

<CodeGroup>
  ```ts TypeScript theme={"languages":{"custom":["languages/helixql.json"]}}
  import {
    serializeQueryBundle,
    writeQueryBundleToPath,
    deserializeQueryBundle,
    readQueryBundleFromPath,
  } from "@helix-db/helix-db";

  const bundle = queries.buildQueryBundle();
  const json = serializeQueryBundle(bundle);
  await writeQueryBundleToPath(bundle, "queries.json");

  // Reading and validating an existing bundle:
  const loaded = deserializeQueryBundle(json);              // throws if invalid
  const fromFile = await readQueryBundleFromPath("queries.json");
  ```

  ```python Python theme={"languages":{"custom":["languages/helixql.json"]}}
  from helixdb import (
      deserialize_query_bundle,
      read_query_bundle_from_path,
      serialize_query_bundle,
      write_query_bundle_to_path,
  )

  bundle = queries.build_query_bundle()
  json_body = serialize_query_bundle(bundle)
  write_query_bundle_to_path(bundle, "queries.json")

  # Reading and validating an existing bundle:
  loaded = deserialize_query_bundle(json_body)              # raises if invalid
  from_file = read_query_bundle_from_path("queries.json")
  ```
</CodeGroup>

`deserializeQueryBundle` / `deserialize_query_bundle` validates the bundle `version`
and rejects anything it does not support, so you catch mismatches at load time rather
than at deploy time. The TypeScript, Rust, and Go SDKs serialize at **v5** and accept
both v4 and v5 on read. The Python SDK is still at **v4** and accepts only v4 — a v5
bundle (for example one using row bindings, which Python does not support yet) will not
load in Python.

## Next Steps

<CardGroup cols={2}>
  <Card title="Working with HelixDB" icon="rocket" href="/database/working-with-enterprise">
    Deploy `queries.json` and the runtime workflow.
  </Card>

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

  <Card title="Querying Guide: overview" icon="circle-arrow-left" href="/database/querying-guide/overview">
    Back to the start of the guide.
  </Card>

  <Card title="npm package" icon="npm" href="https://www.npmjs.com/package/@helix-db/helix-db">
    `@helix-db/helix-db` on npm.
  </Card>
</CardGroup>
