Bitemporal

Bitemporal queries: valid time and recorded time

Each node and edge version has two times: valid time, when the fact was true in the world, and recorded time, when the system recorded it. The two can differ, for example when a change is recorded after it took effect.

The same query can be asked at different combinations of the two times. The four combinations below show what the system recorded on March 5 and what it records now, about March 11 and about today.

Interactive demo → Cypher reference 30-second interactive demo · no signup

Fictional example. Drag the two sliders. The top one sets recorded time, when the system recorded the fact. The bottom one sets valid time, when the fact was true in the world. The query under the chart updates to match.

The same query. belief-99 was revised on 2026-06-01 — from HIGH_RISK to LOW_RISK after KYC completed. Ask what the system had recorded before the revision → HIGH_RISK 0.70. Ask what it recorded after → LOW_RISK 0.95.

In this example the risk service updates belief-99 in place: AT VALID '2026-06-01' MATCH (b:Belief {id: 'belief-99'}) SET b.claim = 'LOW_RISK', b.confidence = 0.95. If you use db.reviseBelief instead, each revision is a new node; use db.beliefLineage('belief-99') to walk the revisions.

The two clocks

Valid time and recorded time

InvariantDB stores two times on every node and edge version: when the fact was true (valid time) and when it was recorded (recorded time).

Clock 1 · Valid time

When it was true in the world.

The customer's KYC status became verified on 2026-06-01. That's a fact about the world — it happened whether or not any system captured it in the same instant.

Valid time answers “what was true then?” You can set the start of a fact’s valid time to a past or future date with the AT VALID write prefix. A later AT VALID write ends the earlier value’s valid time at the new date. Every change is kept in the history log.

// Customer was verified in the world on: 2026-06-01T09:15:00Z
Clock 2 · Recorded time

When the system learned it.

The KYC document didn't arrive at the risk service until 2026-06-03 at 14:22 UTC — two days later. That's a fact about our record-keeping, not the world.

Recorded time answers “what had the system recorded?” The server sets it when the write is applied, and it cannot be backdated. A correction adds a new version; the earlier version stays in the hash-chained history log.

// The system first learned it on: 2026-06-03T14:22:41Z

One query at four combinations of valid and recorded time

Fix one time and vary the other to see how answers change.

 
VALID = March 11
VALID = today
RECORDED
= March 5
HIGH_RISK · conf 0.70
what we believed then, about then
HIGH_RISK · conf 0.70
what we believed then, about today
RECORDED
= today
HIGH_RISK · conf 0.70
what we believe now, about then
LOW_RISK · conf 0.95
what we believe now, about today
Cypher syntax

AT VALID and AT RECORDED

Attach AT VALID, AT RECORDED, or both to a MATCH pattern. The pattern is matched against the graph as it was at that time.

Query at a recorded time

// belief-99 as the system knew it on March 5. MATCH (b:Belief {id: "belief-99"}) AT RECORDED "2026-03-05T00:00:00Z" RETURN b.claim, b.confidence;

Filter after the temporal clause

// Salient memories as of June 1, filtered on that day's salience. MATCH (m:Memory) AT RECORDED "2026-06-01T00:00:00Z" WHERE m.salience > 0.5 RETURN m;

Combine valid time and recorded time

// What did we know on March 5, about the world as it was on March 11? MATCH (b:Belief {id: "belief-99"}) AT RECORDED "2026-03-05" AT VALID "2026-03-11" RETURN b.claim, b.confidence;
Where they attach. AT VALID and AT RECORDED bind to a MATCH pattern — they immediately follow the pattern and rewind the store before the pattern is executed. They do not attach to WITH, WHERE, or RETURN. Put filters and projections after the temporal clauses.

Backdate a write

Put AT VALID before the first clause to set when the written facts became true. The recorded time is always the time of the write. Without the prefix, a write is valid from the moment it is recorded.

// Customer c-42 became verified on 2025-01-01 (fictional data). AT VALID '2025-01-01' MATCH (c:Customer {id: 'c-42'}) SET c.kycStatus = 'verified';

A later AT VALID write ends the earlier value’s valid time at the new date. A write dated before a value recorded for a later date is refused when it would replace that later version and lose one of its values (WRITE_WOULD_REPLACE_LATER_VERSIONS, from release 1.4.2.5); write the change at or after the later date. A query without a time answers what is valid now, so a value dated in the future is returned from its date on (from release 1.4.2.4). Closed intervals such as AT VALID FROM … TO … are refused with CLOSED_INTERVAL_WRITE_UNSUPPORTED. A DELETE ends the fact at the time of the delete, or at the AT VALID time when the statement has one; a delete that would end a fact before it starts is refused. On a query that only reads, the prefix is applied as the valid time of each MATCH that has no temporal clause of its own.

Other temporal clauses

AS OF t is the same as AT RECORDED t. OVER VALID BETWEEN a AND b and OVER RECORDED BETWEEN a AND b return one row per version that overlaps the window. The atTime field on POST /graphs/{name}/cypher and on the MCP cypher_query tool applies AT VALID to each top-level MATCH; it is refused when a MATCH already has a temporal clause or when the query has no top-level MATCH.

Read the time columns

n._valid_from, n._valid_to, n._txn_from, n._txn_to, and n._version return milliseconds since the Unix epoch. _valid_to and _txn_to are null while the version is open. Integer arguments to AT VALID and AT RECORDED are read as nanoseconds, so pass ISO-8601 strings in queries.

MATCH (c:Customer {id: 'c-42'}) RETURN c.kycStatus, c._valid_from, c._valid_to, c._txn_from;
Anatomy of a decision

A decision on March 5, reviewed later

Fictional example. On March 5 the risk service returned HIGH_RISK for a customer and the application declined them. After KYC completed on June 1, the same customer is LOW_RISK. Both values are stored; the later one does not erase the earlier one.

What the system knew at the time.

Ask the store to rewind to March 5. The KYC document hadn't arrived yet. belief-99 was on revision 1, backed by two source episodes. The service returned HIGH_RISK with 0.70 confidence — and the decline queue kicked in.

MATCH (b:Belief {id: "belief-99"}) AT RECORDED "2026-03-05T00:00:00Z" RETURN b.claim, b.confidence;
→ claim = HIGH_RISK · confidence = 0.70 · recorded rev 1

What the system knows today.

Without a temporal clause, the query returns the current value, written after KYC verified the customer on June 1. Confidence is 0.95. The earlier value is still in the history log and can be read with AT RECORDED.

MATCH (b:Belief {id: "belief-99"}) RETURN b.claim, b.confidence;
→ claim = LOW_RISK · confidence = 0.95 · recorded rev 2
Which query answers which question. The March 5 query explains the decline. The query without a temporal clause shows the customer’s current state. The history log holds each recorded version between them.

Interactive demo

Move the recorded-time and valid-time controls and see the query result change. No signup required.