Reference implementation for NEP-001: Native Bitemporal Graph Support.
Cypher procedures and functions that implement AS OF query semantics for Neo4j —
the ability to query the graph as it existed at any point in time, using
relationship-level valid time properties.
Neo4j is excellent at modelling things that change over time — ownership transfers, regulatory reassignments, equipment moves, contract changes. But querying the graph as it existed at a specific point in time requires verbose, error-prone boilerplate:
-- Current approach: manual temporal filter on every relationship
MATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company)
WHERE (r.effective_date IS NULL OR r.effective_date <= $asOf)
AND (r.end_date IS NULL OR r.end_date >= $asOf)
RETURN c.nameAt multi-hop depth this becomes unmanageable:
-- Three-hop traversal: temporal filter duplicated for every relationship
MATCH (w:Well)-[r1:OWNED_BY]->(c:Company)-[r2:SUBJECT_TO]->(a:Authority)
WHERE (r1.effective_date IS NULL OR r1.effective_date <= $asOf)
AND (r1.end_date IS NULL OR r1.end_date >= $asOf)
AND (r2.effective_date IS NULL OR r2.effective_date <= $asOf)
AND (r2.end_date IS NULL OR r2.end_date >= $asOf)
RETURN w.name, c.name, a.nameThis is the boilerplate that native AS OF support should eliminate. This
library demonstrates what that looks like with today's Neo4j.
Three building blocks that implement AS OF semantics:
Drop-in replacement for the manual IS NULL OR filter in WHERE clauses:
-- Before:
MATCH (w:Well)-[r:OWNED_BY]->(c:Company)
WHERE (r.effective_date IS NULL OR r.effective_date <= '2024-06-15')
AND (r.end_date IS NULL OR r.end_date >= '2024-06-15')
-- After:
MATCH (w:Well)-[r:OWNED_BY]->(c:Company)
WHERE temporal.activeAt(r, '2024-06-15')Returns all relationships of specified type(s) that were active at a given date:
MATCH (w:Well {wellId: $id})
CALL temporal.asOf.relationships(w, 'OWNED_BY', $asOf)
YIELD rel, other, effectiveDate, endDate
RETURN other.name AS owner, effectiveDate AS sinceTraverses the graph following only relationships active at a given date:
-- Multi-hop AS OF traversal — temporal consistency across all hops
MATCH (w:Well {wellId: $id})
CALL temporal.asOf.traverse(w, 'OWNED_BY,SUBJECT_TO', $asOf, 3)
YIELD nodes, relationships, length
RETURN [n IN nodes | n.name] AS path| Function | Signature | Returns | Description |
|---|---|---|---|
temporal.activeAt |
(rel, asOf) |
Boolean |
True if relationship was active at the date |
temporal.period |
(rel) |
String |
Formats the valid period: "2020-01-15 → present" |
temporal.overlaps |
(rel, from, to) |
Boolean |
True if relationship was active at any point in the range |
| Procedure | Description |
|---|---|
temporal.asOf.relationships(node, types, asOf) |
Relationships of given type(s) active at the date |
temporal.asOf.traverse(node, types, asOf, depth) |
Traverse via active relationships only |
temporal.changepoints(node, types) |
All START/END events ordered chronologically |
The library reads two relationship properties by convention:
| Property | Meaning | Null behaviour |
|---|---|---|
effective_date |
Date this relationship became valid | Null = "from the beginning of time" |
end_date |
Date this relationship ceased to be valid | Null = "still active / open-ended" |
Values may be stored as Neo4j DATE, DATETIME, or ISO-8601 strings. All three
are handled transparently.
Relationships with no temporal properties are always considered active. This ensures backward compatibility — existing graphs without temporal annotations are fully supported.
MATCH (w:Well {wellId: $id})
CALL temporal.asOf.relationships(w, 'OWNED_BY', '2024-06-15')
YIELD other, effectiveDate
RETURN other.name AS owner, effectiveDate AS sinceMATCH (w:Well {wellId: $id})
CALL temporal.changepoints(w, 'OWNED_BY')
YIELD changeDate, changeType, other
RETURN changeDate, changeType, other.name AS company
ORDER BY changeDateMATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company)
WHERE temporal.overlaps(r, '2023-01-01', '2023-12-31')
RETURN c.name, temporal.period(r) AS periodMATCH (w:Well {wellId: $id})
CALL temporal.asOf.traverse(w, 'OWNED_BY,SUBJECT_TO_JURISDICTION,GRANTED_BY', '2022-10-01', 4)
YIELD nodes, length
RETURN [n IN nodes | coalesce(n.name, n.wellId, n.facilityId)] AS chain, length
ORDER BY lengthMATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company)
RETURN c.name AS company, temporal.period(r) AS validPeriod
ORDER BY r.effective_datecp target/neo4j-temporal-graph-1.0.0.jar $NEO4J_HOME/plugins/
# Restart Neo4jVerify:
MATCH (n)-[r]->(m) WHERE temporal.activeAt(r, date()) RETURN count(r) AS activeNowRequirements: JDK 21+, Maven 3.9+
git clone https://github.com/DIGITAL-FABRIC-AI/neo4j-temporal-graph.git
cd neo4j-temporal-graph
mvn clean package
# → target/neo4j-temporal-graph-1.0.0.jarWithout a local JDK:
./build.sh # Docker-based buildmvn clean testThe test suite uses a well ownership graph with three sequential ownership periods:
Well ──[OWNED_BY 2018-01-01 → 2020-06-30]──► Alpha Corp
Well ──[OWNED_BY 2020-07-01 → 2022-12-31]──► Beta Inc
Well ──[OWNED_BY 2023-01-01 → (present) ]──► Gamma Ltd
Well ──[SUBJECT_TO (no dates) ]──► Alberta (Province)
Tests cover: point-in-time ownership, open-ended relationships, boundary dates, relationships with no temporal properties, multi-type traversal, change history, period formatting, and range overlap.
This library is the reference implementation for NEP-001: Native Bitemporal
Graph Support — a proposal to add AS OF to Cypher as a first-class language
construct:
-- Proposed native syntax (NEP-001):
MATCH (w:Well {wellId: $id})-[r:OWNED_BY]->(c:Company) AS OF $asOf
RETURN c.name
-- Equivalent using this library today:
MATCH (w:Well {wellId: $id})
CALL temporal.asOf.relationships(w, 'OWNED_BY', $asOf)
YIELD other
RETURN other.nameThe proposal covers:
AS OFCypher syntax for single and multi-hop patterns- Valid time (user-defined) and transaction time (system-managed) dimensions
- Temporal index support for period overlap queries
- SQL:2011 alignment
See NEP-001.md for the full proposal.
This library was developed and validated against a production Neo4j graph (2026.01.4 Community Edition) with:
- 3,143,576
SUBJECT_TO_JURISDICTIONrelationships (entities → administrative areas) - 943,763
GRANTED_BYrelationships (licences → regulatory authorities) - 94.6% of jurisdiction relationships with temporal annotations
- ~94,000 relationships with
end_date(representing licence abandonments)
Every temporal query in that system previously required the manual IS NULL OR
filter. This library eliminates that boilerplate and demonstrates the semantics
are correct on real data.
| Component | Version |
|---|---|
| Neo4j | 2026.01.x (Community and Enterprise) |
| Java | 21+ |
Apache License 2.0 — see LICENSE
Ashley Dunfield github.com/DIGITAL-FABRIC-AI