Skip to main content
Guide
HelixDB accepts Cypher statements alongside its native operation-tree queries. Both languages run against the same graph, transactions, and indexes, so data written with one is visible to the other, and existing databases need no migration. Cypher support is a profile of openCypher, not full compatibility. The clauses, expressions, and functions listed under What is supported run. Everything in What is not supported is rejected with a specific error before the statement reads or writes anything. Cypher as described here is served by local servers running the ghcr.io/helixdb/helixdb:v0.0.13 image or later, and by Helix Cloud gateways.
The SDK examples below require SDKs built from the repository checkout. TypeScript 3.2.0, Python 0.3.4, Rust 3.0.0, and Go v0.3.1 do not include the Cypher methods. With those releases, use the HTTP API to run and explain statements.

Run a statement

Send one statement, with optional parameters and a query name, to POST /v2/cypher. The server works out whether the statement reads or writes.
A successful response has ordered columns and rectangular rows, with one value per column in column order:
query is required; parameters and query_name are optional. Duplicate rows are kept unless the statement uses DISTINCT, and row order is only defined by ORDER BY. A write without RETURN succeeds with empty columns and rows. Parameters hold values. Labels, relationship types, and variable names are part of the statement, so keep values in parameters instead of building them into the query text.

Over HTTP

A local server has no authentication:
A Helix Cloud gateway takes the same body. Authenticate as for POST /v2/query, with the cluster API key and, on a GA shared gateway, the database ID:
The gateway routes each statement by what it does: reads go to readers, and writes go to the elected writer. A read-only API key can read and explain, but receives 403 for a statement that writes. The HTTP API reference describes the headers, status codes, and lossless value encoding in full.

From the CLI

From a linked local project, helix cypher runs a statement against the local instance:

Request options

The native request options apply to Cypher too: Rust and TypeScript set options on request_builder() / requestBuilder() before .cypher(...); Go and Python pass them to Cypher and cypher. A warm-only read that Helix Cloud answers with 204 No Content returns no rows. Embedded clients reject request options.

Explain a statement

POST /v2/cypher/explain takes the same body and plans the statement without running it, so explaining a write changes nothing. The plan reports the statement’s effect (Read or Write), its bindings, the selected access paths such as index lookups, estimated cardinalities, blocking work, Cartesian products, and optimizer warnings. Planning checks parameters, so supply every parameter the statement uses.

What is supported

Create and read a small graph:
The WHERE of a WITH filters the rows that clause returns, after its DISTINCT, ORDER BY, SKIP, and LIMIT: WITH x ORDER BY x LIMIT 3 WHERE x > 2 keeps at most the third row. After SKIP or LIMIT, only the variables the WITH projects remain in scope.

What is not supported

These constructs are valid openCypher but outside the profile. Each is rejected before anything runs, with the error category unsupported_feature and the details.detail code shown, unless the table says otherwise. Some of these fail as ordinary compile errors instead:
  • CREATE INDEX ... is a syntax_error (unexpected_syntax). Create indexes with the native secondary, vector, and text index APIs; Cypher planning then uses compatible indexes automatically.
  • A relationship in CREATE needs exactly one type and a direction, otherwise the statement is a syntax_error (no_single_relationship_type).
  • A parameter map in a MATCH pattern, such as MATCH (n:User $props), is a syntax_error (invalid_parameter_use). Write the properties as a map literal, such as {email: $email}.
  • An unrecognized function name is a syntax_error (unknown_function).
  • A statement that is also invalid openCypher reports the standard error, such as variable_already_bound for a CREATE that redeclares a variable.
Vector search, full-text search, and index management have no Cypher syntax; use native queries for them. Cypher is served over HTTP, gRPC, the SDKs, and the CLI. The Bolt protocol and Bolt drivers are not supported.

Values and parameters

Nulls, booleans, strings, lists, maps, and finite floats use ordinary JSON values, in parameters and in results. Integers inside JavaScript’s safe range use JSON numbers. Larger integers and non-finite floats use tagged values: Node and relationship IDs are unsigned decimal strings in separate namespaces; keep them as strings. id() reports an overflow when an ID does not fit a signed 64-bit integer. Relationship endpoints keep their stored direction even when a path traverses them in reverse. Internal metadata is left out of property maps. Scalars, lists, and maps, including their tagged forms, can be passed as parameters; nodes, relationships, and paths cannot. The SDKs keep tagged values in results. TypeScript also accepts bigint parameters and sends them tagged.

Writes and transactions

Each statement that writes runs in one transaction, including its index changes. If any clause fails, nothing the statement wrote is kept.
  • A created node needs exactly one non-empty label, and a created relationship exactly one non-empty type. MATCH without a label scans all nodes.
  • Stored properties are scalars or lists of one scalar type. Maps and nested lists can be used in expressions but not stored, so storing one fails the statement.
  • SET n += {...} updates the given keys and keeps the rest. SET n = {...} replaces every user property. Setting a property to null removes it. SET items run in order, so later items see earlier changes.
  • A plain DELETE of a node that still has relationships fails the statement. DETACH DELETE removes the relationships and the node.
  • Index rules still apply: a value an index cannot store fails the statement, including a string too large for an index key (about 64 KiB).
  • Do not retry a write automatically when its commit outcome is uncertain; see error handling.

Functions, aggregation, and grouping

  • toInteger() and toInt() convert decimal strings exactly, including scientific notation, and truncate fractions toward zero: toInteger('9007199254740993.0') returns 9007199254740993. Invalid text and values outside the signed 64-bit range return null.
  • avg() ignores nulls, returns null for an empty group, and returns a float. A large total does not overflow while the inputs are finite. sum() reports integer overflow.
  • Expressions in RETURN or WITH without aggregates define the groups. In an expression that mixes aggregation with other operations, everything outside the aggregate’s arguments must be a constant, a parameter, a grouped variable, or a projected property, following the openCypher grouping rules. An ambiguous aggregation is rejected before anything runs.
  • ORDER BY can use an aggregate without returning it, as in RETURN 1 + sum(amount) AS total ORDER BY 2 - sum(amount).
  • A simple CASE value WHEN ... evaluates value once, and null never matches null; test for a missing value with CASE WHEN value IS NULL THEN .... Only the selected branch is evaluated, but aggregate arguments inside CASE are always evaluated.

Indexes and performance

Cypher plans use existing native indexes automatically. You do not name an index in the statement; explain shows which one was chosen.
  • An equality on a labeled node, such as MATCH (u:User {email: $email}) or WHERE u.email = $email, can read an equality index. So can IN lists of up to 64 values and ORed equalities on one property.
  • A range such as WHERE u.age >= 21 AND u.age < $max can read a range index, and stops reading as soon as a LIMIT is satisfied.
  • ORDER BY on a range-indexed property in the index’s direction, with a range bound and a LIMIT, reads the index in order instead of sorting.
  • id(n) = $id and id(n) IN $ids read those nodes directly.
  • An equality against an earlier variable, as in UNWIND $ids AS k MATCH (u:User {uid: k}), looks up the index once per row.
  • An expansion only reads the relationship types it names, so -[:WROTE|LIVES_IN]-> skips a node’s other relationships.
  • LIMIT stops scans and expansions early when the clauses before it cannot fail. Reading a stored property in RETURN can fail, so return whole nodes, or apply WITH ... LIMIT first and read properties afterwards: MATCH (u:User) WITH u LIMIT 10 RETURN u.name.
An index is only used when every other condition on that node cannot fail. Comparisons, string predicates, label and null tests, and their AND, OR, XOR, and NOT combinations over literals, parameters, variables, and properties qualify. Arithmetic and function calls keep the label scan.

Limits

A statement that exceeds a budget fails with resource_limit (HTTP 429) instead of returning a truncated result, and a write that fails this way commits nothing. UNWIND range(...) streams its values, while range() in an expression builds the whole list first.

Errors

A failed statement returns the Cypher error body:
error is the category and details.detail the specific condition, both in lower snake case. phase is compile (nothing ran) or runtime, and span gives the byte range of the statement text that failed, when known. Request, authentication, storage, and transaction failures use the native error envelope. The error reference lists every category with its HTTP status and retry guidance.

Next steps

HTTP API

Request headers, status codes, and the OpenAPI contract for the Cypher routes.

Error handling

Cypher error categories, HTTP statuses, and when to retry.

Reading data

The native read API, with Cypher equivalents.

Secondary indexes

Create the indexes that Cypher plans read.