helix-query-json-dynamic
Helix Dynamic Query JSON
Use this skill for inline dynamic query requests sent directly to POST /v1/query.
The inline query body is a JSON serialization of the Rust DSL AST. Every variant an agent can send is documented in the companion files. If you are writing anything beyond a trivial read, open REFERENCE.md first — do not guess variant names or field shapes.
Reference Files
REFERENCE.md— complete AST variant catalog (everyStep,Predicate,Expr,PropertyValue,IndexSpec,RepeatConfig,BatchCondition, envelope field). Use when writing a non-trivial request.EXAMPLES.md— working end-to-end JSON bodies: reads, writes, vector/text search,Repeat,Choose,Coalesce,Union, aggregations, upserts,ForEach, index management, warming. Copy the closest scenario as a starting point.
When To Use
Use this skill when the task is to:
- build a dynamic Helix request body
- debug a failing
POST /v1/querycall - add
parameter_typesto a dynamic request - send
DateTimeor typed-array parameters correctly - understand read versus write behavior on the dynamic route
- use query warming on a dynamic read
- translate a Rust DSL query you already have into its JSON form
Do not use this skill as the main guide for writing stored Rust DSL query functions. Use helix-query-authoring for that.
First Steps
Before writing the payload:
- Decide whether this should really be a stored route instead of a dynamic route. If the request is part of steady-state application traffic, prefer a stored route.
- Confirm whether the request is a read or a write. A query that contains any mutation step (
AddN,AddE,SetProperty,RemoveProperty,Drop,DropEdge,DropEdgeLabeled,DropEdgeById, or anyCreate*Index/DropIndex) must userequest_type: "write". - Confirm whether the inline
queryobject already exists in code, a test, or a serialized payload — prefer copying a known-good shape. - Identify any parameters that need explicit typing, especially
DateTimeand typed arrays.
Required Envelope Rules
- send requests to
POST /v1/query request_typemust be"read"or"write"(lowercase — the enum uses#[serde(rename_all = "lowercase")])querymust be a single inline route object (aReadBatchorWriteBatch), not the fullqueries.jsonbundleparametersis optionalparameter_typesis optional until you need schema-aware coercion (see Parameter Typing)X-Helix-Warm: trueis an optional request header, valid only for reads
Canonical Request Shape
{
"request_type": "read",
"query": {
"queries": [
{
"Query": {
"name": "node_exists",
"steps": ["Count"],
"condition": null
}
}
],
"returns": ["node_exists"]
},
"parameters": {
"name": "Alice",
"entity_id": 123
},
"parameter_types": {
"name": "String",
"entity_id": "I64"
}
}
Notes on the shape:
querycontains aReadBatch(orWriteBatch); both have{ "queries": [...], "returns": [...] }.- Each element of
queriesis aBatchEntry— either{"Query": {...}}or{"ForEach": {...}}. "steps": ["Count"]is a valid step list:Countis a unit variant so it serializes as a bare string. Data-carrying variants are wrapped:{"Limit": 10},{"Has": ["name", {"String": "Alice"}]}, etc.
Serde Encoding Rules
Every encoding in REFERENCE.md follows these rules. Internalize them or the request will fail with unknown variant / invalid type errors.
-
Default encoding is externally tagged. Given a Rust enum
E::Var(..)without a#[serde(...)]attribute:- Unit variant (no data): bare string.
Step::Count→"Count".Predicate::HasKeyis a tuple-with-data variant, not unit — see rule 2. - Tuple variant with 1 field:
{"Var": <inner>}.Step::N(NodeRef::Ids(vec![644]))→{"N": {"Ids": [644]}}. - Tuple variant with 2+ fields:
{"Var": [a, b, ...]}.Predicate::Eq("status", PropertyValue::String("active"))→{"Eq": ["status", {"String": "active"}]}.Predicate::Between("score", 60, 100)→{"Between": ["score", {"I64": 60}, {"I64": 100}]}. - Struct variant:
{"Var": {"field": ...}}.Step::VectorSearchNodes { label, property, ... }→{"VectorSearchNodes": {"label": "...", "property": "...", ...}}.
- Unit variant (no data): bare string.
-
Three enums are
#[serde(untagged)]— no variant wrapper:BatchQuery(the value of the envelope'squeryfield): write theReadBatch/WriteBatchobject inline. There is no{"Read": ...}wrapper.Projection(element of aProjectstep's list): write the inner struct directly.PropertyProjection→{"source": "name", "alias": "name"}.ExprProjection→{"alias": "age_plus_one", "expr": {...}}. Do not write{"Property": {...}}or{"Expr": {...}}wrappers.DynamicQueryValue(values inside the top-levelparametersmap): bare JSON."limit": 25,"tags": ["a","b"],"user": {"name": "Alice"}. No{"I64": 25}wrapping here — that form isPropertyValue, which is inside the AST, not at parameter-value position.
-
DynamicQueryRequestTypeisrename_all = "lowercase": use"read"/"write", never"Read"/"Write". -
Optional fields may be omitted or set to
null.tenant_value,condition,else_traversal,emit_predicate, and similar all serialize viaskip_serializing_if = "Option::is_none"when unset, but the server accepts explicitnull. -
PropertyValueis distinct fromDynamicQueryValue. Inside the AST (literals inHas,Eq,AddNproperties wrapped inPropertyInput::Value, etc.) values are tagged:{"String": "..."},{"I64": 42},{"Bool": true},{"F64": 3.14},{"F64Array": [0.1, 0.2]},{"Null": null}is wrong — use the bare string"Null"for the unit variant. At parameter-value position (top-levelparametersmap) values are untagged bare JSON. -
DateTimeover JSON: supply an RFC3339 string or epoch-millis integer as the parameter value, and declareparameter_types: {"p": "DateTime"}. No implicit coercion — a plain string parameter without the type declaration is just a string. -
Bytesis not round-trippable. The builder raisesUnsupportedBytesParameter. Do not sendBytesparameters through the JSON dynamic route.
Envelope Decision Table
| Goal | request_type |
query.queries[*] shape |
Notes |
|---|---|---|---|
| Simple read | "read" |
{"Query": {"name": "...", "steps": [...], "condition": null}} |
|
| Conditional step after prior step | "read" or "write" |
{"Query": {..., "condition": {"VarNotEmpty": "prev"}}} |
Conditions: VarNotEmpty, VarEmpty, VarMinSize, PrevNotEmpty |
| Single mutation | "write" |
{"Query": {...}} with a mutation step |
See EXAMPLES.md §Write |
| Upsert | "write" |
Multi-entry: load → VarNotEmpty update → VarEmpty create |
See EXAMPLES.md §Upsert |
| Per-row iteration over a param | "read" or "write" |
{"ForEach": {"param": "items", "body": [...]}} |
param must be typed ["Array", "Object"] |
| Warm a read | "read" |
normal body + header X-Helix-Warm: true |
Returns 204 No Content on success |
AST Quick-Map
Step categories and their JSON form (one-liners). Full signatures in REFERENCE.md.
Sources (start a traversal):
{"N": {"Ids": [1,2]}}/{"N": {"Var": "x"}}/{"N": {"Param": "ids"}}— nodes by id / variable / parameter{"NWhere": <SourcePredicate>}— nodes matching a source-safe predicate{"E": {...}}/{"EWhere": <SourcePredicate>}— edges{"VectorSearchNodes": {"label":"...","property":"...","query_vector":{...},"k":{...},"tenant_value":{...}}}{"TextSearchNodes": {...}}— BM25 on nodes{"VectorSearchEdges": {...}},{"TextSearchEdges": {...}}
Traversal (navigate):
{"Out": "LABEL"}/{"Out": null}— alsoIn,Both,OutE,InE,BothE(same shape)"OutN"/"InN"/"OtherN"— unit variants, from an edge stream back to a node
Filters:
{"Has": ["prop", {"String": "v"}]}— property equals{"HasLabel": "User"},{"HasKey": "email"}{"Where": <Predicate>}— full predicate"Dedup"— unit variant{"Within": "var"},{"Without": "var"}— set ops against a stored variable{"EdgeHas": ["weight", {"Value": {"I64": 1}}]},{"EdgeHasLabel": "KNOWS"}
Limits:
{"Limit": 10},{"Skip": 5},{"Range": [0, 25]}— literal{"LimitBy": {"Param": "n"}},{"SkipBy": ...},{"RangeBy": [<StreamBound>, <StreamBound>]}— runtime
Variables:
{"As": "x"}/{"Store": "x"}— name the current stream{"Select": "x"}— replace stream with a stored var{"Inject": "x"}— inject var into stream (source or mid-traversal)
Ordering:
{"OrderBy": ["created_at", "Desc"]}— single property{"OrderByMultiple": [["priority", "Desc"], ["name", "Asc"]]}
Aggregation:
{"Group": "status"},{"GroupCount": "status"}{"AggregateBy": ["Sum", "price"]}— functions:Count,Sum,Min,Max,Mean
Branching (each branch is a SubTraversal = {"steps": [...]}):
{"Union": [{"steps":[...]}, {"steps":[...]}]}{"Choose": {"condition": <Predicate>, "then_traversal": {"steps":[...]}, "else_traversal": null}}{"Coalesce": [{"steps":[...]}, ...]}{"Optional": {"steps":[...]}}
Repeat:
{"Repeat": {"traversal": {"steps":[{"Out":null}]}, "times": 3, "until": null, "emit": "After", "emit_predicate": null, "max_depth": 100}}emitis one of"None","Before","After","All"
Projections (terminal):
{"Values": ["name", "email"]}{"ValueMap": ["$id", "name"]}or{"ValueMap": null}for all{"Project": [{"source":"name","alias":"name"}, {"alias":"age_plus_one","expr":{"Add":[{"Property":"age"},{"Constant":{"I64":1}}]}}]}— no{"Property":...}/{"Expr":...}wrapper (untagged)"EdgeProperties"— unit variant
Terminals (scalar result):
"Count","Exists","Id","Label"
Mutations (write-only):
{"AddN": {"label": "User", "properties": [["name", {"Value": {"String": "Alice"}}]]}}{"AddE": {"label": "FOLLOWS", "to": {"Ids":[42]}, "properties": []}}{"SetProperty": ["name", {"Value": {"String": "Bob"}}]},{"RemoveProperty": "temp"}"Drop"— delete current nodes & their edges{"DropEdge": {"Ids": [42]}},{"DropEdgeLabeled": {"to": {...}, "label": "X"}},{"DropEdgeById": {"Ids": [7]}}
Indexes (write-only):
{"CreateIndex": {"spec": <IndexSpec>, "if_not_exists": true}},{"DropIndex": {"spec": <IndexSpec>}}- Legacy vector/text convenience steps:
{"CreateVectorIndexNodes": {...}},CreateVectorIndexEdges,CreateTextIndexNodes,CreateTextIndexEdges
Reserved (currently no-ops — safe to include but have no effect): "Fold", "Unfold", "Path", "SimplePath", {"WithSack": <PropertyValue>}, {"SackSet": "prop"}, {"SackAdd": "prop"}, "SackGet".
Virtual Fields
Available in projections, value_map, and Has predicates without being declared in your schema:
$id— node or edge id$label— node or edge label$distance— on vector / text search hits; order is ascending (smaller = closer)$from,$to— on edge streams (includingedge_properties) and edge vector/text hits
Distance lifecycle: $distance is present on the direct hit stream produced by VectorSearchNodes / VectorSearchEdges / TextSearchNodes / TextSearchEdges. It is lost once traversal steps off the hit stream (Out, In, Both, OutN, InN, OtherN). Project the distance before navigating if you need to keep it.
Parameter Typing Rules
Use parameter_types when Helix must coerce JSON into a specific parameter type. Every type string is a QueryParamType.
Type string encoding
Unit scalars serialize as bare strings:
"Bool" | "I64" | "F64" | "F32" | "String" | "DateTime" | "Bytes" | "Value" | "Object"
Array is a single-field tuple variant — it wraps its element type:
{"Array": "String"} // array of strings
{"Array": {"Array": "F64"}} // array of arrays of F64
{"Array": "Object"} // array of objects
Required any time the value needs a non-default interpretation: DateTime, typed scalar coercion, or arrays whose element shape the runtime must know.
DateTime
{
"parameters": {"created_after": "2026-04-05T10:00:00Z"},
"parameter_types": {"created_after": "DateTime"}
}
Accepted value forms: RFC3339 string, epoch-millis integer. No implicit coercion — a plain string parameter without the type declaration is just a string.
Typed array example
{
"parameters": {"statuses": ["active", "pending"]},
"parameter_types": {"statuses": {"Array": "String"}}
}
Vector array example
{
"parameters": {"query_vector": [0.12, 0.44, 0.91]},
"parameter_types": {"query_vector": {"Array": "F64"}}
}
Unsupported Bytes
Do not send Bytes parameters through the JSON dynamic route. The builder raises UnsupportedBytesParameter and the gateway cannot round-trip the shape.
Read Versus Write Rules
request_type: "read"— no mutation / index step may appear anywhere in the AST.request_type: "write"— allowed to mix read steps and mutation / index steps in the same batch.
Dynamic requests do not support a "mcp" request type. That's only for the stored-route / MCP tool surface.
If the inline AST contains a write step, the request must also be marked "write" — the gateway uses request_type to pick the transaction kind.
Query Warming
Dynamic query warming uses the same request body plus the header:
X-Helix-Warm: true
Rules:
- only supported for reads
- rejected for writes
- successful warm requests return
204 No Content
Practical Workflow
- Prefer a stored route if the query is stable and production-facing.
- If using the dynamic route, locate or generate the exact inline
queryAST first — either serialize from a RustDynamicQueryRequest::read(...).to_json_string()or copy from a test fixture. - Add
parametersonly for the names the AST expects. - Add
parameter_typesforDateTime, typed arrays, and any other parameters needing schema-aware coercion. - Validate that the body contains one inline route object, not a full query bundle.
- If warming, ensure the request is read-only and add
X-Helix-Warm: true.
Anti-Patterns
Do not:
- send the full
queries.jsonfile underquery— send a single route (theReadBatch/WriteBatchinline) - use
"mcp"as the dynamic request type - capitalize
"Read"/"Write"inrequest_type— the enum is lowercase - rely on implicit
DateTimeparsing withoutparameter_types - send
Bytesparameters - invent inline AST variant names such as
N.Idwhen the parser expectsN.Ids,N.Var, orN.Param. The parser rejects withunknown variant 'Id', expected one of 'Ids', 'Var', 'Param'. Same foot-gun forHas(single vs array),OrderByordering (always[prop, Order], not{prop: Order}), andProjectentries (no{"Property": ...}/{"Expr": ...}wrapper — the enum is untagged). - hand-wave typed array encoding if you have not verified it locally — copy from
tests/register_metadata_tests.rsor a recorded request - wrap
Projectionentries with a variant tag —Projectionis#[serde(untagged)] - wrap top-level parameter values with variant tags —
DynamicQueryValueis untagged (bare JSON) - default to dynamic queries for stable production traffic
Validation Checklist
Before finishing:
- target endpoint is
POST /v1/query -
request_typeis"read"or"write"(lowercase) -
queryis a single inline route object (aReadBatchorWriteBatch), not a bundle -
queries[*]entries are{"Query": {...}}or{"ForEach": {...}}, eachQueryhasname,steps,condition - unit-variant steps are encoded as bare strings (
"Count","Dedup","Exists","Id","Label","OutN","InN","OtherN","EdgeProperties","Drop") - tuple-variant steps with 2+ fields use arrays (
{"Has": ["name", {"String": "v"}]}) - struct-variant steps use objects (
{"VectorSearchNodes": {"label": ...}}) -
Projectentries have no variant wrapper (untagged enum) - inner AST values use tagged
PropertyValue({"I64": 1}); top-levelparametersvalues are bare JSON (1) -
parameter_typescovers every parameter that needs typed coercion (DateTime, typed arrays) -
DateTimeparameters are RFC3339 strings or epoch-millis integers and declared inparameter_types - no
Bytesparameters - warming is only applied to reads
- if the AST contains any mutation or index step,
request_typeis"write"
Source References
Authoritative source files (for when the reference answer is ambiguous):
src/lib.rs:2506-2962—Stepenum (every variant)src/lib.rs:1548-1596—Predicate;src/lib.rs:1603-1626—SourcePredicatesrc/lib.rs:1352-1384—Exprsrc/lib.rs:972-1002—PropertyValue;src/lib.rs:1197-1202—PropertyInputsrc/lib.rs:1232-1339—NodeRef/EdgeRefsrc/lib.rs:1888-1965—PropertyProjection/ExprProjection/Projection(untagged)src/lib.rs:2250-2323—RepeatConfig;src/lib.rs:1984-1993—EmitBehaviorsrc/lib.rs:2327-2398—IndexSpecsrc/lib.rs:4041-4078—BatchCondition,BatchEntry,NamedQuerysrc/lib.rs:4089-4270—ReadBatch/WriteBatch/BatchQuery(untagged)src/lib.rs:4346-4452—DynamicQueryRequestType(lowercase),DynamicQueryValue(untagged),DynamicQueryRequestsrc/query_generator.rs:9-31—QueryParamTypetests/register_metadata_tests.rs:182-186, 243-245, 274-275— ground-truth serialized examples
More from helixdb/skills
helix-query-authoring
Write and revise HelixDB Rust DSL stored queries from scratch. Use when the task is to add, update, or review a Helix query built with read_batch, write_batch, traversal builders, projections, indexes, BM25 text search, or vector search. Inspect local labels, edges, properties, and existing query patterns before inventing new code. See REFERENCE.md for the full builder catalog and EXAMPLES.md for end-to-end patterns.
10helix-query-optimize
Review and improve HelixDB query performance and query shape. Use when the task is to optimize a slow Helix query, improve anchor choice, tighten index usage, reduce traversal breadth, slim projections, fix BM25 or vector search scope, or decide between stored and dynamic routes.
8helix-query-from-gremlin
Translate Gremlin and TinkerPop-style traversals into HelixDB Rust DSL stored queries. Use when the input contains Gremlin, TinkerPop, g.V, g.E, hasLabel, has, out, in, both, outE, inE, repeat, emit, dedup, valueMap, count, range, or limit and the goal is to produce an equivalent Helix Rust query.
7helix-query-from-cypher
Translate Cypher and Neo4j-style queries into HelixDB Rust DSL stored queries. Use when the input contains Cypher, Neo4j, MATCH, OPTIONAL MATCH, WHERE, RETURN, ORDER BY, LIMIT, DISTINCT, MERGE, CASE, UNWIND, FOREACH, DETACH DELETE, IS NULL, or variable-length path patterns and the goal is to produce an equivalent Helix Rust query.
7