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, 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 returnhelix.Request, set the query name with helix.ReadQuery("name") or
helix.WriteQuery("name"), and declare runtime parameters inline on the query
builder.
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:
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.
TypeScript
Python
| 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:- Bound to a step argument that takes a
ParamRef- for.limit(p.limit),.skip(p.skip), etc. The proxy returned bydefineParams/define_paramsis the value to pass. - Inside a predicate - TypeScript offers
Predicate.eqParam("prop", "param_name")and the rest of the*Paramfamily for string-keyed parameter refs. Python can pass theParamRefdirectly, for examplePredicate.eq("tenantId", p.tenant_id). - As a property value or expression -
PropertyInput.param("name"),Expr.param("name"),NodeRef.param("name"), andEdgeRef.param("name")all take the name as a string. In Python,p.tenant_id.input()is a shorthand when a method expects aPropertyInput.
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:
Shipping a parameterized query
A query built withreadBatch() / 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.
.toDynamicJson(...) / .to_dynamic_json(...) is
the body for POST /v1/query. Below are the five 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:
| 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 |
{ 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:
.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 withclient.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.
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.
/v1/query:
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 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:
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:
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.
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:
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
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 / deserialize_query_bundle 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: dynamic queries, transactions, and the HTTP surface.
Querying Guide: overview
Back to the start of the guide.
npm package
@helix-db/helix-db on npm.