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.So far every parameterized example on the prior pages has shown
p = paramsObject / Predicate.eqParam("prop", "name") / toDynamicJson(params, values)
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 the three
distinct ways to ship a parameterized query to the runtime.
Declaring parameters with defineParams
defineParams({...}) takes a record of parameter names to ParamSchema constructors
and returns a typed proxy. Each property of the returned proxy is a ParamRef you
can pass into builder methods that expect one.
param.* constructors:
| Constructor | Wire type | Accepted JS values |
|---|---|---|
param.bool() | Bool | boolean |
param.i64() | I64 | number (safe int) or bigint |
param.f64() / param.f32() | F64 / F32 | number |
param.string() | String | string |
param.dateTime() | DateTime | DateTime, RFC3339 string, or epoch-millis number/bigint |
param.bytes() | Bytes | Uint8Array or number[] — dynamic requests reject this |
param.value() | Value | any JSON value (untyped) |
param.object(inner?) | Object | Record<string, T> if inner is given, else loose JSON |
param.array(inner) | {Array: inner} | array of values matching the inner schema |
param.array(param.f32()) is what produces the {"Array": "F32"} shape required for
vector parameters; param.object(param.string()) produces {"Array": "Object"} rows
inside a forEachParam.
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:- Bound to a step argument that takes a
ParamRef— for.limit(p.limit),.skip(p.skip), etc. The proxy returned bydefineParamsis the value to pass. - Inside a predicate —
Predicate.eqParam("prop", "param_name")and the rest of the*Paramfamily take the parameter name as a string, not the proxy. - As a property value or expression —
PropertyInput.param("name"),Expr.param("name"),NodeRef.param("name"),EdgeRef.param("name"). All take the name as a string.
defineParams, 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:
Three deployment shapes
A query built withreadBatch() / writeBatch() can be shipped to the runtime in
three different ways. They share the same query AST; they differ in where and when
the parameters bind.
1. Dynamic request
The simplest case: build the batch, call.toDynamicJson(params, values), POST the
resulting body to /v1/query. No deploy step. Best for ad-hoc queries, prototyping,
and one-off integrations.
.toDynamicJson(params, values) is the body for
POST /v1/query. Below are the three equivalent ways to send it:
--data-binary @file.json —
the inline --data-raw form works for small bodies but gets unwieldy fast.
The three serialization helpers on a batch:
| Method | Returns |
|---|---|
.toDynamicJson(params?, values?) | JSON string (full envelope: request_type, query, parameters, parameter_types) |
.toDynamicRequest(params?, values?) | DynamicQueryRequest instance — useful when you want to mutate it before serializing |
.toDynamicBytes(params?, values?) | Uint8Array of the JSON encoding — handy for fetch bodies that prefer bytes |
.toJsonString() returns the raw batch (no envelope, no parameters) for
deployment as a stored route — covered below.
2. Stored bundle
For routes you want to deploy alongside the rest of your application, wrap each query function withregisterRead 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.
read_routes.<name> / write_routes.<name> is exactly what .toJsonString()
would have produced for the underlying batch — no envelope. Once deployed, callers
invoke the routes by name at POST /v1/query/<name>, passing the parameters as the
JSON body.
Route names must be unique across reads and writes; duplicates throw GenerateError
at bundle time.
Calling a stored route — the wire shape is much smaller than the dynamic envelope
because the route is already registered. The body is just the parameter object.
POST /v1/query/add_user with {"username": "...", "tier": "..."} as the body. The response shape is the object keyed by the bound
names in .returning([...]).
3. Typed call helpers
defineQueries(...) also exposes a typed call map. Each entry is a function that
takes the route’s parameter values (inferred from defineParams) and returns a
serializable DynamicQueryRequest. The argument is fully type-checked from the
parameter schema; unknown keys, missing keys, and wrong value types are all compile
errors:
/v1/query/<name>) or the dynamic
route (/v1/query) — but the conventional path is to POST it as the dynamic
envelope so the bundled route name does not need to exist on the server yet:
queries.call.* is most useful in tests, in scripts, and when iterating
locally before the bundle is deployed.
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 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:
Number handling: bigint for i64
JavaScript number only has 53 bits of integer precision. The 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:
stringifyJson, .toJsonString(), and serializeQueryBundle all preserve bigint
without loss. Plain JSON.stringify does not — use the package’s serializers when
your 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.
param.dateTime(), the call helpers accept
DateTime, an RFC3339 string, or an epoch-millis number / bigint:
Bytes parameters: stored only
param.bytes() is declarable in defineParams for the bundle, but the dynamic
envelope cannot round-trip a Uint8Array faithfully. Attempting to
toDynamicJson(params, { payload: someBytes }) or to call a registered route with a
bytes parameter through queries.call.* throws
DynamicQueryError.UnsupportedBytesParameter.
If you need bytes, deploy the query as a stored route and pass the value through a
binary-aware client (the runtime’s RPC interface). Stored routes happily accept
Bytes; only the JSON dynamic envelope is the bottleneck.
Generating the bundle from a script
Thequeries.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:
deserializeQueryBundle validates version === 4 (the current bundle version); it
will reject older or newer bundles so you catch mismatches at load time rather than
at deploy time.
Next Steps
Working with HelixDB
Deploy
queries.json and the runtime workflow.Querying overview
Conceptual model: stored vs dynamic queries, transactions, and the HTTP surface.
Querying Guide: overview
Back to the start of the guide.
npm package
@helixdb/enterprise-ql on npm.