Docs/Timebase Components/openCypher Query Reference
AtlasReference

openCypher Query Reference

The complete Atlas Cypher reference: every clause, operator, literal form, built-in function and pattern feature the query engine supports.

Updated 2 Nov 2026Timebase Docs

Overview#

Atlas executes a read subset of openCypher against the compiled World - the in-memory graph built by PUT /api/v1/branches/{branch}/domains/{domain}/world. The engine is purpose-built for that graph, so its supported set is defined here rather than by any other Cypher implementation.

This article is the complete language reference: every clause, operator, literal form, built-in function and pattern feature. If you want to know where to run a query and which patterns solve common modelling questions, read Querying with Cypher instead. This one answers "is X supported, what is its arity, and what does it return".

Two things to know before reading further:

  1. Atlas Cypher is a read language. CREATE, MERGE, DELETE, SET and REMOVE return an error at parse time.
  2. A few constructs parse and are not applied, so the query returns rows that are wider than you intended. Those are listed first.

Conventions that shape your results#

Each item below parses cleanly and returns HTTP 200. Read this section before you write a query.

Convention What you write What it returns
An inline map compares whole values MATCH (n:Pump {Speed: 42}) No rows, because a bound property is a compound and the map compares the whole three-part value. Name the part: MATCH (n:Pump {Speed.Value: 42}), or WHERE n.Speed.Value = 42.
No list subscript labels(n)[0] The whole list, such as ["urn:atlas:ontology:lonestar-plant:Plant"]. Read the last segment of the address.
The display name is .Label n.name Null. The universal display name is n.Label, with a capital L.
A bound property is a compound WHERE n.Speed > 1500 No rows, because the comparison is against the whole three-part value. Write n.Speed.Value.
ORDER BY after UNION … UNION … ORDER BY l Each branch is sorted and the branches are then stacked. Sort the combined result in the consumer.
DISTINCT inside collect() collect(DISTINCT n.Label) The full list. Only count() applies DISTINCT.
=~ is unanchored WHERE n.Label =~ 'P-[0-9]+' Matches anywhere in the value. Anchor with ^ and $.
Variable-length depth MATCH (a)-[:contains*]->(b) 100 hops, which is also the engine maximum. A bound above it is rejected, not clamped.
The durable id is entityId n.instanceId Null. Use n.entityId.
Property names are case-sensitive n.speed A null column. Short-name resolution compares the last segment of the address exactly.
coalesce() coalesce(a, b) 400 Query.Unsupported. Use CASE WHEN x IS NULL THEN d ELSE x END.
valueAt() reads the current value valueAt(v.Temp, $ts) The property's current value. For history, UNWIND the property.

Finding nodes with no outgoing relationship#

Return the full OPTIONAL MATCH result and select the empty rows in your consumer:

MATCH (n:Pump)
OPTIONAL MATCH (n)-[:contains]->(child)
RETURN n.Label AS Pump, child.Label AS Child

A WHERE following an OPTIONAL MATCH is folded inside the optional, so a WHERE r IS NULL test inside the query is not a supported form and returns HTTP 500.


The query endpoint#

POST /api/v1/branches/{branch}/domains/{domain}/queries

Atlas serves its single-page application for any URL outside the API surface, so a mistyped route returns 200 with an HTML body rather than a 404. If a response body starts with <!doctype html>, check the route and the access token before you check the query.

Request body#

Field Type Required Notes
cypher string yes The query text. The field is cypher, not query.
parameters object no $name bindings. JSON values are unwrapped from JsonElement to native CLR scalars before use.
projections bool no Opt-in enrichment. Absent or false gives the plain columnar shape.
{
  "cypher": "MATCH (n:Pump) WHERE n.FlowRate.Value > $minFlow RETURN n.Label, n.FlowRate.Value",
  "parameters": { "minFlow": 100.0 }
}

Sending {"query": …} returns 400 invalid-request with detail Cypher query string is required.

Response#

Default shape:

{
  "columns": ["n.Label", "n.FlowRate.Value"],
  "data": [["North CT-01", 142.5], ["North CT-02", 138.0]]
}

columns are derived from the RETURN item text when no AS alias is given: a variable yields its name, a property access yields var.Prop, a function call yields fn(arg) with DISTINCT prefixed inside the parens when present, and count(*) yields count(*).

With "projections": true:

{
  "columns": ["n"],
  "data": [[ … ]],
  "classesTouched": ["urn:atlas:ontology:lonestar-plant:Plant"],
  "instanceIdsByClass": {
    "urn:atlas:ontology:lonestar-plant:Plant": ["Lonestar-Plant"]
  }
}
  • classesTouched is an array of full type URNs.
  • instanceIdsByClass maps type URN to durable instance id strings, not integers. The field name is retained for wire compatibility.
  • Enrichment applies to entity-valued columns only. A scalar projection such as RETURN n.Label returns classesTouched: [] and instanceIdsByClass: {}.

Authentication#

/api/v1 requires a Pulse bearer whose aud includes Atlas. An unauthenticated call does not return a clean 401 - it returns 302 to the Pulse OIDC endpoint and ultimately an HTML sign-in page. A client that follows redirects receives HTML where it expected JSON.

The /app twin#

POST /app/branches/{branch}/domains/{domain}/queries exists with an identical body shape with an identical body shape. It is the cookie-authenticated route the Atlas interface uses for its own queries, is not published in the OpenAPI documents, and is not an integration surface. It is listed here so it can be identified in a browser network trace.


Lexical structure#

Element Rule Notes
Keyword case options { caseInsensitive = true; } MATCH, match and Match are all valid. Function names are lower-cased before dispatch, so toUpper and TOUPPER both work.
Identifier ID: LetterOrDigit+ Letters include _ and everything above U+007F.
Escaped identifier Backticks Backticks are stripped. Use them for names containing spaces, and for a full type URN used as a label, relationship type or property name - (n:`urn:atlas:ontology:plant:Pump`) - which cannot be written bare because of its colons.
String literal CHAR_LITERAL (single quotes) and STRING_LITERAL (double quotes), Both are valid string delimiters. Escapes \\ \' \" \n \t \r \b \f are unescaped.
Integer / float DIGIT, FLOAT Decimal, hex (0x…), octal, exponent and f/d suffix forms parse. An integer that fits int becomes an int, otherwise long; anything else becomes a double.
Boolean / null TRUE, FALSE, NULL
List literal listLit: LBRACK expressionChain? RBRACK [1, 2, 3], [].
Map literal mapLit Parses, then throws Map literals are not supported.
Block comment /* … */ Routed to a COMMENTS channel, ignored.
Line comment // … Ignored.
Statement terminator script: query SEMI? EOF A single trailing ; is allowed. Only one statement per request.

Supported clauses#

Clause Supported Notes
MATCH yes matchSt: OPTIONAL? MATCH patternWhere. A WHERE binds to the preceding MATCH.
OPTIONAL MATCH yes Emits a null-padded row when nothing matches. A trailing WHERE is folded inside the optional, so select the empty rows in the consumer.
WHERE yes Only attachable to MATCH and WITH. There is no WHERE after UNWIND or after RETURN.
RETURN yes With DISTINCT, AS aliases, *, aggregation.
WITH yes withSt: WITH projectionBody where?. The only way to filter on an aggregate.
ORDER BY yes ASC / ASCENDING / DESC / DESCENDING. Part of projectionBody, so it attaches to a RETURN or a WITH.
SKIP yes
LIMIT yes
UNWIND yes UNWIND expression AS symbol. Also the entry point for historical range reads.
UNION yes Deduplicates (UnionOperator wraps UnionAllOperator).
UNION ALL yes In a multi-UNION query the last ALL specifier wins for the whole query.
CALL { … } yes Sub-query block. Three hard constraints, below.
CREATE no CREATE clauses are not supported
MERGE no MERGE clauses are not supported
DELETE no DELETE clauses are not supported
SET no SET clauses are not supported
REMOVE no REMOVE clauses are not supported
CALL procedure() no CALL procedure invocations are not supported. Use CALL { ... } block form.
Standalone CALL no CALL statements are not supported
YIELD no Reachable only through the two CALL forms above.

Expressions outside the supported set#

Each of these returns the message shown.

Construct Message
XOR XOR expressions are not supported
^ (power) Power (^) expressions are not supported
Pattern comprehension [(a)-[:R]->(b) | b] Pattern comprehensions are not supported
A pattern in a non-MATCH position, e.g. WHERE NOT (a)-[:feeds]->() Pattern expressions in non-MATCH context are not supported
EXISTS { … } EXISTS subquery expressions are not supported
Map literal {a: 1} Map literals are not supported
UNION inside CALL { } CALL { ... } sub-queries must not contain UNION.
Any unrecognised function name Function '<name>' is not supported
List index / slice x[0], x[1..3] (no message - the whole list is returned; see the conventions above)

Patterns#

Node patterns#

MATCH (n)                    -- every node
MATCH (n:Pump)               -- nodes whose archetype is Pump or a subclass of Pump
MATCH (:Pump)                -- anonymous, still filtered
MATCH (n:Machine:Active)     -- both labels must hold; same as (n:Machine) WHERE n:Active
MATCH (n:Pump {Label: 'P-001'})  -- same as (n:Pump) WHERE n.Label = 'P-001'
MATCH (n:Pump WHERE n.Speed.Value > 1500)  -- any predicate, inside the brackets
  • Label matching is OWL-inheritance aware. MATCH (n:Equipment) also returns Pumps and Tanks when they are declared subclasses.
  • A label that resolves to no archetype is refused: HTTP 400, Query.SemanticError, Unknown label 'Pumps': no class in the compiled model matches it by URI or local name. A type that exists but has no instances is still a known label and returns an empty result. Earlier 2.0 builds returned an empty result, 200, for an unknown label; if you see that, you are on one of them.
  • Every label on a node pattern filters, and all of them must hold. Property short names on that variable resolve against the first label's type hierarchy.
  • An inline property map is an equality on each key, combined with the WHERE that follows the MATCH; {Label: 'X', Area: $a} means n.Label = 'X' AND n.Area = $a. Anything other than equality goes in WHERE. A bound property is a compound, so a map compares the whole three-part value; a dotted key names a part, {Speed.Value: 42}, the same as WHERE n.Speed.Value = 42. A whole map passed as a parameter, (n $props), and a map on a variable-length relationship are refused with Query.Unsupported. Earlier 2.0 builds dropped inline maps and returned the unfiltered rows; if a map has no effect, you are on one of them.

Label names in MATCH are either the short local name (the segment after the final colon of the type URN) or the full URN in backticks: MATCH (n:`urn:atlas:ontology:plant:Pump`). labels() and keys() return full URNs, so either strip the prefix before feeding one back into a pattern or backtick the whole value.

Relationship patterns#

MATCH (a)-[:contains]->(b)      -- outgoing
MATCH (a)<-[:feeds]-(b)         -- incoming
MATCH (a)-[:connectsTo]-(b)     -- either direction
MATCH (a)--(b)                  -- any type, either direction
MATCH (a)-[r]->(b)              -- bind the edge to r
MATCH (a)-[:A|:B]->(b)          -- type A or type B
MATCH (a)-[:A|B]->(b)           -- the second colon is optional
MATCH (a)-[r:feeds {Medium: 'steam'}]->(b)        -- equality on an edge property
MATCH (a)-[r:feeds WHERE r.Diameter > 50]->(b)    -- any predicate, inside the brackets

Direction is derived from the presence of < and >. Each relationship type token expands to its inheritance subtree. A token is the short local name or the full URN in backticks ([:`urn:atlas:ontology:atlas-common:contains`]). A token that matches no relationship type is refused: HTTP 400, Query.SemanticError, Unknown relationship type 'feeds': no relationship type in the compiled model matches it by URI, local name or inverse name.

A type written on any node of a pattern filters that node, and the match is inheritance aware, exactly as it is on the start node. MATCH (line:Line)-[:contains]->(p:Pump) returns only the pumps of each line, and MATCH (line:Line)-[:contains]->(p) WHERE p:Pump is an equivalent form. Earlier 2.0 builds applied only the type on the start node; if a type on the far end of an arrow is ignored, you are on one of them.

Variable-length paths#

Form Min Max
[*] 1 100
[*3] 3 3
[*1..3] 1 3
[*1..] 1 100
[*..5] 1 5

Those are the five forms the parser accepts. Execution is a bounded depth-first search with trail semantics: the same relationship is never traversed twice within one path.

100 hops is the engine ceiling, and an open-ended form now walks all of it rather than stopping at a lower hidden default. Expansion visits each edge at most once per source row, so depth costs you traversal work rather than compounding it.

A bound above the ceiling is refused at analysis time with a 400, rather than being quietly reduced to something you did not ask for:

Variable-length upper bound 250 exceeds the engine maximum of 100 hops.
Variable-length lower bound 250 exceeds the engine maximum of 100 hops - no path can satisfy it.

State the depth you need in the pattern anyway. A query that says what it means is cheaper to read than one relying on the ceiling, and it does not change behaviour if the ceiling ever moves.

A relationship variable on a variable-length pattern is a list of the edges on the path, in traversal order, as in Cypher. Read a property per edge with a list comprehension, or constrain the hops with a list predicate:

MATCH (a:Plant)-[r:contains*1..3]->(b)
WHERE all(x IN r WHERE x.Active.Value = true)
RETURN b.Label, [x IN r | x.Diameter] AS Diameters, size(r) AS Hops

r.Diameter on such a variable is refused at analysis time, because a list has no properties; the message names the two forms above. A property map or a bracket WHERE on a variable-length relationship is refused for the same reason: write the predicate over the list. Earlier 2.0 builds bound the variable to the last edge only; if r.Diameter returns a value on a variable-length pattern, you are on one of them.

Relationship properties#

A relationship variable's properties are read the same way a node's are, and they work in WHERE, ORDER BY and aggregation:

MATCH (a:Pump)-[r:feedsInto]->(b:Tank)
WHERE r.Diameter > 50
RETURN b.Label, r.Diameter
ORDER BY r.Diameter DESC

The predicate can also sit inside the brackets, next to the edge it filters: MATCH (a:Pump)-[r:feedsInto WHERE r.Diameter > 50]->(b:Tank) is the same query. A node pattern takes a bracket WHERE too, (p:Pump WHERE p.Speed.Value > 1500). In both places it is combined with the clause's own WHERE, and under OPTIONAL MATCH a row whose match fails the predicate is still emitted with nulls.

Property names on a relationship resolve against that relationship type's own properties. Where a node type and a relationship type both carry a short name, each resolves within its own scope, and an ambiguous name is reported as a diagnostic rather than silently returning null.

Named paths#

MATCH p = (a:Plant)-[:contains*1..3]->(b)
RETURN length(p), nodes(p), relationships(p)

length, nodes and relationships return null when their argument is not a path.

Shortest paths#

MATCH p = shortestPath((a:Pump)-[*]->(b:Tank))
MATCH p = allShortestPaths((a:Pump)-[*]-(b:Tank))

Three constraints, all enforced by the analyzer. The first two carry these verbatim messages:

  • shortestPath/allShortestPaths requires exactly one relationship pattern: (node)-[*]->(node)
  • shortestPath/allShortestPaths requires a variable-length relationship [*]

The third is a minimum hop count above 1, which is rejected rather than applied. shortestPath((a)-[:NEXT*3..]->(b)) does not mean "the shortest path of at least three hops"; there is no such search, so asking for one is refused instead of quietly returning the plain one-hop answer. Only 0 and 1 are accepted as a minimum, and both mean the same thing here.

A floor in an ordinary variable-length match is unaffected: MATCH (a)-[:NEXT*3..]->(b) still honours it. The restriction is specific to shortest-path search.

shortestPath enforces the types on both endpoints, like every other pattern, whether or not an earlier clause bound the endpoint. allShortestPaths returns every joint-shortest route.


Operators#

Comparison#

Operator AST Semantics
= ComparisonOp.Equal Numeric-coercing: both sides numeric compares as double. Strings compare ordinal. Null on either side is false, except null = null which is true.
<> NotEqual
< > <= >= LessThan … Any null operand yields false, never null.
=~ Regular expression Unanchored - anchor with ^ and $. Non-string operands yield false.

(ComparisonHelper).

Boolean#

AND, OR, NOT. XOR parses and is rejected. There is no IS TRUE / IS FALSE.

Arithmetic#

Operator Behaviour
+ If either side is a string, the result is string concatenation. Otherwise integral+integral stays integral (long), any float promotes to double.
- * Type-preserving in the same way.
/ Integer division truncates. 7/2 = 3. Division by zero returns null, not an error, and the row then disappears from a numeric filter.
% Same integral/float rule; modulo by zero returns null.
unary - Type-preserving.
^ Rejected: Power (^) expressions are not supported.

(ArithmeticHelper). The integer-division rule is the most common source of a report that reads 0 on every row. Multiply by 100.0 before dividing.

Predicates#

Predicate Form Notes
IS NULL x IS NULL
IS NOT NULL x IS NOT NULL Cannot detect "no connector data": a bound property always materialises as a non-null compound.
IN x IN [a, b] Element-wise Equal. Non-enumerable right-hand side yields false.
STARTS WITH s STARTS WITH 'P-' Ordinal. Non-string operands yield false.
ENDS WITH s ENDS WITH '-01' Ordinal.
CONTAINS s CONTAINS 'Pump' Ordinal.
Label predicate n:Plant, n:A:B Boolean, OWL-inheritance aware, ALL labels must match. Equivalent to writing the labels on the node in the pattern.
IS TYPE n IS TYPE True when the slot holds an ArchetypeRef. Also switches the planner into schema mode - see below.
IS INSTANCE n IS INSTANCE True when the slot holds an EntityRef. Does not switch the scan; instance scanning is already the default.
IS ANY n IS ANY Compiles to the constant true. A no-op filter.

String predicates: (StringHelper). Label predicate: FunctionHelper.HasLabels,. Kind predicates:, CompileIsKind.

CASE#

Both forms are supported, with or without ELSE. A generic CASE treats a non-boolean, non-null WHEN result as true; a missing ELSE yields null.

-- generic
CASE WHEN n.Speed.Value > 100 THEN 'high' WHEN n.Speed.Value > 50 THEN 'medium' ELSE 'low' END

-- simple
CASE n.Status WHEN 'Running' THEN 1 ELSE 0 END

CASE is the replacement for the non-existent coalesce() CASE WHEN x IS NULL THEN $default ELSE x END.

List comprehension#

[x IN range(1, 10) WHERE x % 2 = 0 | x * x]
[n IN nodes(p) | n.Label]
[r IN relationships(p) | type(r)]
[n IN nodes(p) | CASE WHEN n.Label STARTS WITH 'P-' THEN 'pump' ELSE 'other' END]

[x IN list], [x IN list WHERE p], [x IN list | expr] and the full form are all valid. The iteration variable can appear anywhere in the predicate or the projection: a property read, a function argument, a CASE, a nested comprehension. A non-enumerable list expression yields null. Pattern comprehension (a pattern instead of a variable) is rejected.

List predicates#

all(x IN list WHERE predicate)      -- every element satisfies it; true for an empty list
any(x IN list WHERE predicate)      -- at least one does; false for an empty list
none(x IN list WHERE predicate)     -- no element does; true for an empty list
single(x IN list WHERE predicate)   -- exactly one does

The WHERE is required. They work over any list: a literal, nodes(p), relationships(p), a variable-length relationship variable, or UNWIND output, in WHERE as well as RETURN. A null or non-list value yields null, which a WHERE treats as false. A predicate that evaluates to null counts as false for its element.

MATCH p = (a:Plant)-[:contains*1..3]->(b)
WHERE all(x IN relationships(p) WHERE x.Active.Value = true)
RETURN b.Label

Parameters#

Reference with $. Two spellings are accepted:

WHERE n.Area = $area      -- named
WHERE n.Area = $0         -- positional/numeric name

The numeric form is a name, not an index: the request body must contain a key literally called "0".

Accepted value types: string, number, boolean, list, object, null. JSON is unwrapped once, before planning, by NormalizeParameters: integral JSON numbers prefer long over double, arrays become List<object?>, objects become Dictionary<string, object?>, Null and Undefined both collapse to null. datetime(null) returns default(DateTime), which is 0001-01-01, not null - an inconsistency with the other temporal coercions.

Parameters are recommended for two reasons:

  • Injection safety. Values never enter the query text.
  • Plan-cache reuse. The plan cache is keyed by raw Cypher text, so a parameterised query is parsed and analysed once and then reused; string-interpolated queries miss the cache on every distinct value. The cache lives on the CypherEngine instance, which is discarded and rebuilt when the domain is recompiled.

There is no cost-based planner. Parameters do not let the planner "optimise independently of values" - in fact the opposite is true for pushdown, which runs per execution precisely so that parameter values can be folded into literal predicate operands.


Projection: RETURN, WITH, ORDER BY, SKIP, LIMIT#

projectionBody : DISTINCT? projectionItems orderSt? skipSt? limitSt?

ORDER BY, SKIP and LIMIT are part of the projection body, so they attach to a RETURN or a WITH, in that order, and cannot appear on their own.

RETURN n.Label                            -- single property
RETURN n.Label AS Pump, n.Speed.Value     -- alias
RETURN DISTINCT n.Area
RETURN n                                  -- the whole entity, see below
RETURN *                                  -- all in-scope variables

WITH is the pipeline stage. Use it to filter on an aggregate, since WHERE cannot be applied to count() directly:

MATCH (p:Pump)<-[:contains]-(line)
WITH line, count(p) AS Pumps
WHERE Pumps > 5
RETURN line.Label AS Line, Pumps
ORDER BY Pumps DESC
LIMIT 10

A WITH without aggregation preserves EntityRef / ArchetypeRef in the row so later clauses can still resolve properties through the strategy dispatch path. Only a terminal RETURN converts an entity reference into the dictionary shape below.

The shape of RETURN n#

An entity-valued column is projected into a dictionary:

Key Value
entityId The durable instance URN, taken from the @entity:Uri column. Falls back to the ephemeral integer id only if that column is absent.
typeId The archetype's full type URN.
row The entity's row index in its archetype. Internal, unstable.
entityLabel The display name, redirected out of @entity:Label into its own reserved key so a user property named Label can never shadow it.
each property Keyed by the local name (last colon segment). Compound parents, including one whose fields are Composed, come back as one nested value, not as dotted keys.

A bound property's value is a three-part compound:

"PlantBoilerEfficiency": { "Timestamp": "2026-08-07T19:35:45.513939Z", "Value": 0.8905615918065714, "Quality": 192 }

Quality does bind today. Seven connectors emit an IntColumn("Quality"), and 192 is Good. Note the URI is atlas-common:Quality, and its datatype is now xsd:integer.

A type-valued column (from IS TYPE schema mode) projects as {typeId, label} (EnrichArchetypeRef,).

Virtual properties#

Resolved by GetVirtualProperty when the name matches no registered component.

Expression Slot holds Returns
n.entityId EntityRef The durable instance URN. This is the property to store or pass back to the API.
n.typeId EntityRef or ArchetypeRef The archetype's full type URN.
n.row EntityRef Internal row index. Unstable across compiles.
t.label ArchetypeRef only The short local name of the type. Not available on an entity - use n.Label, which is the real @entity:Label component.
c.FieldName CompoundValue A sub-field of a compound, e.g. reading.Timestamp.
n.instanceId anything null. There is no such property.

Any other unmatched name returns null.

UNION#

MATCH (n:Pump)  RETURN n.Label AS Asset
UNION
MATCH (n:Tank)  RETURN n.Label AS Asset

Branches are planned independently and then stacked:

  • All branches must return the same column count, or the query returns All UNION branches must return the same number of columns. Got: 2, 3
  • Column names come from the first branch.
  • ORDER BY, SKIP and LIMIT written after the last branch belong to that branch. Sort and page a combined result in the consumer.
  • A historical range inside a branch (an UNWIND of a bound property with a WITH … WHERE time window, see Historical range queries) is pushed to the source per branch, exactly as it is outside a UNION. Earlier 2.0 builds skipped that pushdown inside a UNION and returned the source's default window or nothing; if a UNION of history returns far fewer rows than its branches do on their own, you are on one of them.

Aggregation#

Six aggregates are implemented.

Function Arity Returns DISTINCT
count(*) 0 integer n/a
count(expr) 1 integer honoured
sum(expr) 1 numeric ignored
avg(expr) 1 numeric ignored
min(expr) 1 same type as input ignored
max(expr) 1 same type as input ignored
collect(expr) 1 list parsed, not applied

avg and round are both available: avg(1.0) returns 1 and round(1.5) returns 2.

Grouping is implicit: every non-aggregated item in the same RETURN or WITH becomes a grouping key.

ORDER BY after an aggregating RETURN is resolved against the aggregated output column names, so sort by the alias (ORDER BY PumpCount DESC), not by a source expression.

The supported aggregates are the six above. stDev, stDevP, percentileDisc and percentileCont return Function 'stDev' is not supported.


CALL { } sub-queries#

MATCH (l:Line)
CALL {
  WITH l
  MATCH (l)-[:contains]->(p)
  WHERE p:Pump
  RETURN count(p) AS Pumps
}
RETURN l.Label AS Line, Pumps

Three constraints apply.

  1. The block must end with a RETURN whose items are all named - either an explicit AS alias or a bare variable reference. Otherwise: CALL { ... } sub-query RETURN items must have explicit aliases (use 'RETURN expr AS name').
  2. The export set must be non-empty: CALL { ... } sub-query must end with a RETURN projection whose items export back to the outer scope.
  3. A leading WITH composed entirely of bare variables is the import specifier. Write it that way - a WITH carrying aliased or computed items imports nothing, and the sub-query then runs against an empty scope. An import naming a variable that does not exist returns CALL { ... } imports undefined variable 'l'.

UNION inside the block is rejected: CALL { ... } sub-queries must not contain UNION.


Built-in functions#

Function names are matched case-insensitively. Anything not in these tables returns Function '<name>' is not supported (400 Query.Unsupported).

Forty-nine names are dispatched, of which four are aliases (toInt, toBool, ceiling, toDateTime), giving 45 distinct scalar functions. Add the six aggregates above and the valueAt special form for 56 callable names in total.

Identity and entity#

Function Arity Returns Notes
id(x) 1 integer, or null Ephemeral engine id. Changes on every compile. Do not store it. Accepts a node, or a reified relationship (returns the edge entity's id).
entityUri(x) 1 string, or null The durable URN of a node or a reified relationship. This is the handle to store.
labels(x) 1 list of string, or null Always exactly one element: the archetype's full type URN, e.g. urn:atlas:ontology:lonestar-plant:Plant. Null for a non-entity.
type(r) 1 string, or null The relationship type name. Null unless the argument is an edge.
startNode(r) 1 node, or null The source endpoint of an edge, rehydrated from the EdgeRef.
endNode(r) 1 node, or null The target endpoint.
properties(x) 1 map, or null All properties keyed by full storage key. Compound parents fold their leaves into one nested value. Null for an edge - use entityUri(r) instead.
keys(x) 1 list of string, or null The same key set as properties(x), names only. Includes @entity:Label. Null for an edge.

FunctionHelper.

Type conversion#

Function Arity Returns Notes
toString(x) 1 string, or null value?.ToString().
toInteger(x) / toInt(x) 1 integer, or null Returns null on unparseable input rather than throwing. Float-shaped strings truncate: '3.7' becomes 3. Floats truncate, they do not banker's-round.
toFloat(x) 1 float, or null Null on unparseable input. Invariant culture.
toBoolean(x) / toBool(x) 1 boolean, or null Accepts a real boolean or the strings true / false. Anything else is null.
;
helpers at and XsdCoercion.ToBooleanOrNull.

Temporal#

This family is how you write a time window without hardcoding raw tick integers.

Function Arity Returns Notes
datetime() 0 datetime The query's single UTC "now" snapshot, captured once per execution context, so every call in one query agrees on the same instant.
datetime(x) / toDateTime(x) 1 datetime Coerces long ticks, DateTime, DateTimeOffset or an ISO-8601 string to UTC. A null argument yields default(DateTime) (0001-01-01), not null.
relative(offset) 1 datetime, or null Applies an offset string to datetime(). See the offset language below.
relative(anchor, offset) 2 datetime or integer ticks, or null Mirrors the anchor's shape: long ticks in gives long ticks out, so stays comparable against a raw ticks column.
ticks(x) 1 integer, or null 100-nanosecond ticks since year 1 UTC. A long passes through unchanged.
unix(x) 1 integer, or null Seconds since 1970-01-01 UTC.
unixms(x) 1 integer, or null Milliseconds since the epoch.
fromTicks(x) 1 datetime, or null Inverse of ticks. Accepts long or int only.
fromUnix(x) 1 datetime, or null Inverse of unix.
fromUnixms(x) 1 datetime, or null Inverse of unixms.
;
FunctionHelper.UtcNow; coercions.
Every coercion is null-in-null-out
and returns null for unparseable input rather than throwing - the sole exception being
datetime(null).

The relative() offset language

RelativeTimeParser. The offset is a string matched against:

^(?<clamp>[yMwdhm])?(?:(?<sign>[+\-])?(?<count>\d+)(?<unit>[yMdhms]))?$

At least one of the clamp or the count group must be present. now is a case-insensitive alias for "no-op" and returns the anchor unchanged.

Part Values Meaning
clamp y M w d h m Truncate the anchor to the start of that interval. w rolls back to Sunday. Note M is months and m is minutes - the clamp is case-sensitive even though keywords are not.
sign + - Defaults to +.
count digits
unit y M d h m s The offset unit. There is no w offset unit, only a w clamp.
Example Result
'-1h' one hour before the anchor
'-90m' ninety minutes before
'h' the top of the current hour
'd' midnight this morning UTC
'd+6h' 06:00 today
'h-2h' the top of the hour, then back two hours
'now' the anchor unchanged

A malformed offset returns null through the TryApply path. The throwing overload used by compile-time folding raises FormatException with the verbatim text:

Invalid relative-time expression: '<offset>'.

String#

Function Arity Returns Notes
toUpper(s) 1 string, or null Invariant culture.
toLower(s) 1 string, or null Invariant culture.
trim(s) 1 string, or null Both ends.
ltrim(s) 1 string, or null Leading only.
rtrim(s) 1 string, or null Trailing only.
left(s, n) 2 string, or null n <= 0 returns ""; n past the end returns the whole string.
right(s, n) 2 string, or null Same clamping.
substring(s, start) 2 string, or null Zero-based. A negative start clamps to 0; a start past the end returns "".
substring(s, start, len) 3 string, or null An over-long len clamps to the remainder.
replace(s, search, repl) 3 string, or null Ordinal, all occurrences. A null search or repl is treated as "".
split(s, delim) 2 list of string, or null Empty entries are kept.
reverse(x) 1 string or list, or null Reverses a list if given a non-string enumerable, otherwise the string.
size(s) 1 integer See the List table - size handles both.
;
StringFunctions. All of these coerce their first
argument with ToString(), so toUpper(42) returns "42" rather than failing.

Math#

Function Arity Returns Notes
abs(x) 1 float, or null Every arm converts to double first.
ceil(x) / ceiling(x) 1 float, or null
floor(x) 1 float, or null
round(x) 1 float, or null MidpointRounding.AwayFromZero. No precision argument.
sign(x) 1 integer, or null The only one that returns an integer.
rand() 0 float Random.Shared.NextDouble(), in [0,1). Not seedable.
;
MathFunctions. Note the return types: the previous
edition implied integer results for ceil/floor/round. They are doubles.

There is no sqrt, log, exp, pow, or trigonometry, and ^ is rejected.

List#

Function Arity Returns Notes
size(x) 1 integer, or null String length or element count. Null for anything else.
head(x) 1 element, or null First element; null for an empty list.
tail(x) 1 list, or null Everything after the first.
last(x) 1 element, or null
range(start, end) 2 list of integer Inclusive of both ends. Step 1.
range(start, end, step) 3 list of integer A negative step counts down. Step 0 returns an empty list, it does not error.
;
ListFunctions.

There is no reduce, extract, filter, nodes-style list slicing, or index access. x[0] parses and is dropped.

Path#

Valid only on a named-path variable.

Function Arity Returns Notes
nodes(p) 1 list of node, or null Null if the argument is not a path.
relationships(p) 1 list of relationship, or null Null if not a path.
length(p) 1 integer, or null Edge count. Also accepts a string (character count) or any enumerable (element count).
;
PathFunctions.

A bare nodes(p) returns internal references. Project the field you want: [n IN nodes(p) | n.Label].


Atlas extensions#

IS TYPE and schema mode#

IS TYPE is the only entry point into the type layer of the graph. It is both a runtime predicate and a planner directive.

MATCH (t:Equipment)-[:contains]->(child)
WHERE t IS TYPE
RETURN t.typeId, t.label, child.typeId

When a labelled node pattern's variable appears in a top-level IS TYPE predicate in the immediately following WHERE, the planner substitutes SchemaScanOperator for LabelScanOperator and SchemaExpandOperator for the instance expanders. The row then holds an ArchetypeRef instead of an EntityRef, which is why t.label and t.typeId work and t.entityId does not.

The trigger is narrow (IsTypeScan / ExpressionContainsIsType,): the predicate must be a direct IS TYPE on that variable, reachable through AND conjunctions only. Putting it behind an OR compiles to a plain runtime type test against an instance scan, which is always false.

IS INSTANCE is the runtime test for the default case and switches nothing. IS ANY compiles to the constant true.

valueAt(entity.property, timestamp)#

valueAt() is not a point-in-time lookup and it never calls a connector.

MATCH (v:Vessel)
RETURN v.Label, valueAt(v.Temperature, $ts).Value AS Temp

What it actually does: resolves the property's compound binding and materialises the compound from the current value populated by the subscription orchestrator. The timestamp argument is coerced to ticks and used only as part of the per-query cache key and to select the active strategy edge. The returned value is the current value regardless of the timestamp you pass.

There is no GetValue on the store contract. IExternalDataStore exposes only GetRangeAsync and GetRowsetAsync, and the interface doc states it outright: "Range/rowset only. Point-in-time scalar bound values come from the current value populated by the subscription orchestrator; no wire pull path remains.". The engine's own integration test is named ValueAt_Basic_PointInTime_ReturnsCurrentSoaCompoundValue and its comment reads "historical lookups are out of scope".

For genuine history, use UNWIND over the compound. That is the only path that reaches a connector.

Argument rules, enforced at parse time and analysis time:

Rule Verbatim message
Exactly two arguments valueAt expects exactly two arguments (property, timestamp); got 3.
First argument must be a property reference on an entity variable valueAt's first argument must be a property reference on an entity variable, e.g. valueAt(v.temperature, ts).
No DISTINCT inside the call valueAt does not support DISTINCT.
The variable must be in scope valueAt references undefined variable 'v'.
The property must exist valueAt references unknown property 'Temperature' on 'v'.
The property must be a compound valueAt requires a bound-compound property. 'Temperature' is not a compound property.
It must not be static-stored valueAt requires a bound or calculated compound property. 'Temperature' is static-stored — it has no temporal history.

Note the DISTINCT rule precisely: valueAt(DISTINCT …) is rejected. RETURN DISTINCT valueAt(…) is a ReturnClause DISTINCT and is unaffected.

The timestamp argument accepts long ticks, DateTime, DateTimeOffset or an ISO-8601 string; it is coerced by XsdCoercion.TicksFrom and a null or unparseable value makes the whole call return null.

Per-query coalescing still holds. Repeated valueAt calls with the same (entityId, componentId, timestamp) are served from one cache entry, so selecting .Value and .Timestamp from the same call costs one resolution.

Sub-field access uses the compound's declared field names. For the shipped atlas-common:TVQ type those are .Timestamp, .Value and .Quality. Accessing a field the compound does not declare raises Compound type '<uri>' has no field named '<name>'.


Historical range queries#

Every temporal bound is expressed in Cypher. The request body carries only cypher, parameters and projections - there are no time-range fields on the API itself.

The UNWIND range shape#

UNWIND over a bound or calculated compound property dispatches a range call to the registered store and applies whatever the store could not satisfy locally.

MATCH (a:Agitator)
UNWIND a.Vibration AS v
WITH a, v
WHERE v.Timestamp >= datetime('2026-08-01T00:00:00Z')
  AND v.Timestamp <  relative('d')
RETURN a.Label AS Asset, v.Timestamp AS Time, v.Value AS Vibration
ORDER BY Time
LIMIT 10000

Two syntactic rules apply:

  • UNWIND takes no WHERE of its own, so stage the window through a WITH as above.
  • The window must therefore be a WITH … WHERE, which is also where a LIMIT that you want pushed down should sit relative to the projection.

The time axis is not hardcoded to Timestamp#

The axis field name comes from whichever compound field declares atlas:isTimeAxis. Timestamp is that field for the shipped atlas-common:TVQ type. A custom compound with a differently-named time field uses its own name, and a compound that declares no axis is not time-segmentable, so the window cannot be pushed down to the source.

What a range result contains#

Range resolution splits the window at any configuration change and stitches the segments back together on the time axis. Two results follow from that:

  • A window spanning a strategy changeover returns segments from each strategy, with a synthesised boundary sample stamped at the changeover instant.
  • A leading bounding sample earlier than your from can appear, so a value is defined at the start of the window. Trim it in the consumer if your calculation needs an exact boundary.

The pushdown contract, for connector authors#

public sealed record PushdownRequest(
    PushdownPredicate? Predicate,
    IReadOnlyList<PushdownSortKey> SortKeys,
    int? Limit,
    int? Skip);

public sealed record PushdownResidual(
    PushdownPredicate? Predicate,
    IReadOnlyList<PushdownSortKey> SortKeys,
    int? Limit,
    int? Skip);
  • There is no TimeFrom, no TimeTo, no OrderByTime and no Predicates. The time window lives inside Predicate, as ComparePredicate nodes over the compound's time-axis FieldRefExpr.
  • Ordering arrives as SortKeys, a list of PushdownSortKey(FieldRefExpr Field, SortDirection Direction), on any field, not only the timestamp.
  • "I satisfied nothing" is PushdownResidual.From(request). "I satisfied everything" is PushdownResidual.Empty. Partial support is a residual with only the unsatisfied members set. PushdownResidual.All and PushdownResidual.Predicates do not exist.

The full connector-side contract is covered in Connector Range Queries. This section explains why a LIMIT sometimes reaches the source and sometimes does not.


Errors#

Every failure is RFC-9457 application/problem+json with a frozen type URI under https://atlas.timebase.io/errors/ and a traceId. There is no {"error": …} body anywhere.

An invalid-request failure additionally carries a stable code extension. Generic failures, including the 500, carry only type, title, status and detail. Branch on the type URI, then on code when it is present.

Scenario HTTP code Detail
cypher missing or blank 400 (none) Cypher query string is required.
Domain never compiled 400 Query.NotCompiled Domain '{domain}' on branch '{branch}' has not been compiled. Call PUT /api/v1/branches/{branch}/domains/{domain}/world first.
Parse error 400 Query.SyntaxError Syntax error at line {line}:{charPositionInLine} - {msg}
Clause, expression or function outside the supported set 400 Query.Unsupported The message naming it, such as Function 'coalesce' is not supported
Semantic error 400 Query.SemanticError The message, such as Variable "x" is not defined
A MATCH label or relationship type that nothing in the compiled model matches 400 Query.SemanticError Unknown label 'Pumps': no class in the compiled model matches it by URI or local name. or Unknown relationship type 'feeds': no relationship type in the compiled model matches it by URI, local name or inverse name.
A property or field read whose type is pinned differently on the types the pattern matches 400 Query.SemanticError Property 'Running.Value' is pinned differently across the types this pattern matches: Pump pins boolean, Tank pins double. Add a label that selects one type.
Runtime failure during execution 500 (none) A generic internal-error problem document. Quote the traceId when you contact support.

Two behaviours that are not errors:

  • An unknown label in a WHERE predicate evaluates to false, 200. WHERE n:Pumps is a boolean test, so it filters every row out rather than failing. Only a label or relationship type written in a MATCH pattern is refused.
  • Querying after a change without recompiling returns the previously compiled world, 200. Only the never-compiled case is Query.NotCompiled. Atlas also auto-recompiles every recorded (branch, domain) pair at startup, so a restart does not require a manual recompile.

Pins on a Defer property#

A property that is still open on the property itself (a union of types, or a data type with a union-typed field) can be pinned on each type that uses it. One property such as Running can then be a boolean on Pump and a double on Tank, and a compiled model keeps a typed column for each type.

A query that reads that property is refused when the types it can match disagree, because the value would have no single type:

MATCH (n) RETURN n.Running.Value
Property 'Running.Value' is pinned differently across the types this pattern matches: Pump pins boolean, Tank pins double. Add a label that selects one type.

This is Query.SemanticError, HTTP 400. It applies when the pattern is unlabelled, has several labels, or has a label whose subtypes pin the leaf differently. Add a label that selects one type and the same read works:

MATCH (n:Pump) RETURN n.Running.Value

A query on a single labelled type is unchanged, and so is a read of a property that every matched type pins the same way. A type that leaves the leaf open is not a competing type, though such a type cannot compile in the first place. To pin a leaf, see Pin a property's leaf on a type.


esc
↑↓ navigate · ↵ open · ⌘K anywhere