Cypher reference
openCypher, with two extra time axes.
InvariantDB targets openCypher syntax for everything you know — MATCH, CREATE, MERGE, SET, DELETE, WITH, UNWIND, aggregates, list comprehensions, quantifiers. On top of that, every property is bitemporal, so AT VALID / AT RECORDED / OVER VALID BETWEEN are first-class keywords.
Reads
// Standard match
MATCH (p:Person {name: 'Alice'})-[:WORKS_AT]->(c:Company)
RETURN p.name, c.name
// At a point in time (real-world)
MATCH (p:Policy) AT VALID '2024-06-01'
WHERE p.holder = 'acme' RETURN p
// As the database knew it at a point in time (transaction)
MATCH (p:Policy) AT RECORDED '2024-06-01' RETURN p
// Both axes at once — "what was true on June 1 according to what we
// knew on June 1"
MATCH (p:Policy)
AT VALID '2024-06-01'
AT RECORDED '2024-06-01'
RETURN p
// Walk every version that overlaps a window
MATCH (p:Policy {id: $id})
OVER VALID BETWEEN '2024-01-01' AND '2024-12-31'
RETURN p.premium, p._valid_from, p._valid_to
ORDER BY p._valid_from
Writes
CREATE (p:Person {name: 'Alice', email: 'alice@example.com'})
MERGE (p:Person {email: 'alice@example.com'})
ON CREATE SET p.created_at = datetime()
ON MATCH SET p.last_seen = datetime()
// Map-merge: keep existing keys, overwrite supplied
SET p += {a: 1, b: 2}
DELETE p
// Cascade across edges
DETACH DELETE p
Aggregation
| Aggregate | Notes |
count(*), count(x) | Empty input → 0 (Cypher spec). |
count(DISTINCT x) | Dedups before counting. |
sum(x), avg(x) | Empty input: sum=0, avg=null. |
min(x), max(x) | Empty input: null. |
collect(x), collect(DISTINCT x) | List collector. |
| WITH-stage aggregation | WITH x, count(*) AS c WHERE c > 5 RETURN x, c works as expected. |
Pattern features
// Variable-length paths
MATCH (a)-[*1..5]->(b) WHERE a.id = $start AND b.id = $end RETURN length(path)
// Named paths
MATCH p = (a)-[r]->(b)
RETURN length(p), nodes(p), relationships(p)
// shortestPath / allShortestPaths
MATCH (a:Person {id:$a}), (b:Person {id:$b})
MATCH p = shortestPath((a)-[*..6]-(b))
RETURN p
// Pattern comprehension
MATCH (a:Person {id:$id})
RETURN [(a)-[:KNOWS]->(friend) WHERE friend.active | friend.name] AS active_friends
// Quantifier predicates
MATCH (p:Project)
WHERE ANY(x IN p.tags WHERE x STARTS WITH 'urgent')
AND ALL(c IN p.checks WHERE c.passed)
RETURN p
// reduce()
RETURN reduce(s = 0, x IN [1, 2, 3] | s + x * x) AS sum_of_squares
WHERE conditions
// Standard comparison
WHERE n.score >= 0.5 AND n.label IN ['A', 'B']
// Regex
WHERE n.name =~ '(?i)alice.*'
// String predicates
WHERE n.name STARTS WITH 'A'
WHERE n.url ENDS WITH '.com'
WHERE n.body CONTAINS 'pricing'
// Not-equals
WHERE a.x <> b.x
// Null handling
WHERE n.email IS NULL
WHERE n.email IS NOT NULL
Aggregating subqueries
// COUNT { ... } — Cypher-5 count subquery
RETURN COUNT { MATCH (a:Person {name:'Alice'})-[:KNOWS]->() } AS friend_count
// CALL { ... } — sub-query with parameters
MATCH (a:Account {id:$id})
CALL {
WITH a
MATCH (a)-[:OWNS]->(p:Position)
RETURN sum(p.value) AS total
}
RETURN a.id, total
Indexes
// Property index — current data
CREATE INDEX FOR (n:Person) ON (n.email)
// Vector index (HNSW)
// Schema-driven via /graphs/{name}/schema; see docs.html
// Drop
DROP INDEX FOR (n:Person) ON (n.email)
Historical backfill: CREATE INDEX now backfills every snapshot in history so
AT VALID '<past>' queries hit the index too. See the migration note in the
ADR index.
Procedures
Time + history
| Procedure | Purpose |
db.changes(from_ns, to_ns) | Every mutation in a window. |
db.delta(from_ns, to_ns) | Per-entity summary of changes in a window. |
db.changeRate(window_seconds) | Mutations-per-second over the recent window. |
db.propertyHistory(nodeId, prop) | Per-property bitemporal version history. |
db.history(nodeId) | Full per-version record dump. |
db.delta(from_ts, to_ts) | Per-entity summary of changes in a window. |
Provenance + lineage
db.propertyProvenance(nodeId, prop) | Provenance (actor / source / reason / confidence) at every version. |
db.linkDerivedFrom(node, sources, kind?) | Wire derivation edges into the graph. |
db.derivedFrom(nodeStrId, maxDepth?) | Walk derivation lineage. |
Agent memory
db.recordEpisode(session, kind, text, payload) | Append an event to a session log. |
db.replayEpisodes(session, limit) | Replay a session's events. |
db.consolidateSession(session, opts) | Roll old episodes into a summary. |
db.sessionSummaries(session) | List summaries for a session. |
db.recordBelief(agent, key, content, evidence) | Record a belief + evidence. |
db.reviseBelief(key, content, evidence) | Revise a belief; previous version stays. |
db.beliefLineage(key) | Every version of a belief, with evidence. |
db.applySalienceDecay(session, opts) | Decay session-wide salience. |
db.reinforceSalience(episodeId, amount) | Boost a single memory. |
db.embeddingModelDrift() | List embedding models in the graph + drift signal. |
Compliance
db.aclEvents(sinceId?, principal?, limit?) | Read ACL audit chain. |
db.verifyAclChain() | Verify the hash chain. |
db.destroySubjectKey(subject) | GDPR Art. 17 — crypto-shred. |
db.proveErasure(subject) | Signed receipt + audit_seq. |
db.subjectExport(subject) | GDPR Art. 20 — full per-subject export. |
db.dpBudget(principal?) | Differential-privacy budget snapshot. |
db.dpEvents(principal?, from_ns?, to_ns?) | DP audit log replay. |
db.dpVerifyChain() | Verify the DP audit hash chain. |
Graph algorithms
gds.shortestPath(source, target, edgeType?) | BFS shortest path. |
gds.pageRank(maxIters?, damping?) | Standard PageRank. |
gds.connectedComponents() | Union-Find components. |
gds.degreeCentrality(direction?) | In/out/total degree. |
gds.fulltextSearch(label, query, k?) | BM25 top-K. |
gds.vectorSearch(label, prop, query, k?) | HNSW top-K. |
gds.semanticMatchExpand(label, prop, query, k, hops, edgeTypes, threshold?) | Top-K seeds + N-hop graph expansion. Single call replaces a RAG pipeline. |
Inline vector functions
// In WHERE
MATCH (d:Doc) WHERE cosine(d.embedding, $query) > 0.75 RETURN d
// In RETURN
MATCH (d:Doc) RETURN d.title, cosine(d.embedding, $query) AS sim
ORDER BY sim DESC LIMIT 10
// Available: cosine, euclidean, dotProduct
When a vector index is declared, the planner pushes the WHERE cosine() > t filter into HNSW automatically.
Parameter substitution
// Anywhere a literal goes — including LIMIT / SKIP
MATCH (p:Person) WHERE p.role = $role RETURN p LIMIT $page_size
EXPLAIN + EXPLAIN ANALYZE
EXPLAIN MATCH (n:Person) WHERE n.email = $e RETURN n
// → planner text, no execution
EXPLAIN ANALYZE MATCH (n:Person) WHERE n.email = $e RETURN n
// → actual row counts + per-stage timing, the query runs
Things to watch
Backticks for reserved-ish labels. :CONTAINS works as a label name; backtick when in doubt: :`weird name with spaces`.
OPTIONAL MATCH chains. If an OPTIONAL MATCH introduces a new variable that you later filter on, make sure the filter handles null; otherwise rows from the optional side disappear.
Pure aggregate RETURN with no rows. Returns one row with identity values (count→0, sum→0, avg→null). This matches Cypher spec but surprises people coming from SQL.
Private-mode keys (DP) refuse non-aggregate returns.
RETURN n →
PRIVATE_MODE_NON_AGGREGATE_RETURN. See
DP details.
Quickstart → ·
SDK → ·
MCP tools → ·
All features →