Skip to content

Time-travel with snapshots and compare

Tutorial · 2 of 5

Every fact in Telha is stamped with three clocks. In this tutorial you record a fact, correct it, and then ask the same question at two different atTxTime coordinates to see how Telha reconstructs what you believed in the past, even after the belief has changed. You finish with /v1/snapshot and /v1/compare.

You will need

  • A running server and an API key (quickstart, steps 2-3)
  • curl
  • Five minutes with the tritemporal model concept page open in another tab, this tutorial mirrors its worked example

Scenario

You track a supplier risk and who owns it. On an early date, an out-of-date org chart says the owner is Sarah. Weeks later, a correction arrives: the owner was actually Michael the whole time. You want to ask "who owned this risk?" two ways: as best known today, and as it was believed before the correction landed.

1. Record the initial belief

Create a RISK record with an owner property. We will use POST /v1/records (not /v1/ingest) here because we want a single addressable record we can version with PUT.

curl -sS -X POST http://127.0.0.1:7625/v1/records \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "records": [
      { "labels": ["RISK"],
        "properties": { "title": "Cold-chain excursion risk", "severity": 6, "owner": "Sarah" },
        "validTime": { "start": 1746057600000000 }
      }
    ]
  }'
Response (201)
{
  "records": [
    { "id": "d41d...", "labels": ["RISK"],
      "properties": { "title": "Cold-chain excursion risk", "severity": 6, "owner": "Sarah" },
      "validTime": { "start": 1746057600000000 },
      "txTime": { "start": 1749513783000000 },
      "tombstone": false, "relationships": [] }
  ]
}

validTime.start above is 2025-05-01T00:00:00Z in microseconds, this record asserts the risk has been true in the world since May 1. txTime.start is stamped automatically at write time, when Telha recorded the belief (Feb 3, in this scenario, if you set your system clock or just treat it as "now" for the exercise).

export RISK_ID=d41d...

Checkpoint

GET /v1/records/$RISK_ID returns the record with owner: "Sarah".

2. Correct the fact

A correction arrives later: the owner was actually Michael, effective the same May 1 valid-time start. Write a new full-snapshot version with PUT:

curl -sS -X PUT http://127.0.0.1:7625/v1/records/$RISK_ID \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "labels": ["RISK"],
    "properties": { "title": "Cold-chain excursion risk", "severity": 6, "owner": "Michael" },
    "validTime": { "start": 1746057600000000 }
  }'
Response (200)
{
  "id": "d41d...", "labels": ["RISK"],
  "properties": { "title": "Cold-chain excursion risk", "severity": 6, "owner": "Michael" },
  "validTime": { "start": 1746057600000000 },
  "txTime": { "start": 1752048600000000 },
  "tombstone": false
}

Nothing was overwritten. The old version (owner: "Sarah") is still there, reconstructable at its original transaction time, this is the append-only discipline the tritemporal model is built on.

Checkpoint

curl -fsS "http://127.0.0.1:7625/v1/records/$RISK_ID/history?limit=10" -H "x-api-key: $KEY"
versions should have two entries: the original (owner: "Sarah") and the correction (owner: "Michael"), newest first.

3. Ask the same question two ways

Query the risk's owner as of world-time May 1, with no atTxTime, current belief:

curl -sS -X POST http://127.0.0.1:7625/v1/query \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"find": "RISK", "where": {"title": {"$eq": "Cold-chain excursion risk"}}, "atValidTime": "2026-05-01T00:00:00Z"}'
owner: Michael
{ "records": [ { "id": "d41d...", "properties": { "owner": "Michael", "severity": 6 }, "...": "..." } ], "...": "..." }

Now pin atTxTime to a moment before the correction, when only the Sarah belief existed:

curl -sS -X POST http://127.0.0.1:7625/v1/query \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"find": "RISK", "where": {"title": {"$eq": "Cold-chain excursion risk"}}, "atValidTime": "2026-05-01T00:00:00Z", "atTxTime": "2026-06-01T00:00:00Z"}'
owner: Sarah (if txTime is pinned before your correction write)
{ "records": [ { "id": "d41d...", "properties": { "owner": "Sarah", "severity": 6 }, "...": "..." } ], "...": "..." }

Match the txTime to your own clock

The exact atTxTime cutoff you need depends on when you actually ran step 2 on your machine, pick any RFC3339 timestamp between your POST /v1/records (step 1) and your PUT (step 2) writes. That is the whole trick: atTxTime reconstructs whatever the winning version was as of that recording date, regardless of what has been corrected since.

Read the two responses side by side:

Query atValidTime atTxTime Owner
"As we know it today" May 1 (omitted, = now) Michael
"As we believed it before the correction" May 1 before the PUT Sarah

This is exactly the worked example in The tritemporal model: a plain last-write-wins store cannot answer the second question at all, the old belief would simply be gone. Telha keeps every version, so "what did we believe before the amendment arrived" stays answerable, forever.

4. Pin a moment with /v1/snapshot

/v1/snapshot is query sugar: it pins atValidTime (required) and optionally atTxTime, then re-dispatches through the same executor as /v1/query.

curl -fsS -X POST http://127.0.0.1:7625/v1/snapshot \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{"find": "RISK", "atValidTime": "2026-05-01T00:00:00Z"}'

The response shape is identical to /v1/query's, records, related, nextCursor, stats, just with the temporal coordinate baked into the request instead of threaded through every call site.

5. Diff two moments with /v1/compare

/v1/compare computes what changed between two bitemporal coordinates: added, removed, and modified, with per-property old and new values.

curl -sS -X POST http://127.0.0.1:7625/v1/compare \
  -H "x-api-key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "baseline": { "validTime": "2026-05-01T00:00:00Z", "txTime": "2026-06-01T00:00:00Z" },
    "comparison": { "validTime": "2026-05-01T00:00:00Z" },
    "find": "RISK"
  }'
Response (200)
{
  "added": [],
  "removed": [],
  "modified": [
    {
      "kind": "node", "id": "d41d...", "labels": ["RISK"],
      "versions": {
        "baseline": { "validTimeStart": 1746057600000000, "txTimeStart": 1749513783000000 },
        "comparison": { "validTimeStart": 1746057600000000, "txTimeStart": 1752048600000000 }
      },
      "props": { "added": {}, "removed": {}, "changed": [{ "key": "owner", "old": "Sarah", "new": "Michael" }] },
      "labelsAdded": [], "labelsRemoved": [], "touch": false
    }
  ],
  "stats": { "added": 0, "removed": 0, "modified": 1, "unchanged": 0 },
  "nextCursor": null
}

Checkpoint

modified[0].props.changed contains exactly one entry: owner went from "Sarah" to "Michael". This is the delta report a governance or audit workflow would consume, in one call, without you having to diff two query responses by hand.

What you learned

  • How to write a fact, then correct it, with POST /v1/records and PUT /v1/records/:id
  • Why corrections never overwrite: the superseded version stays reconstructable
  • How atValidTime alone answers "what is true, as best we know now" and atValidTime + atTxTime answers "what did we believe, as of that recording date, about that world-date"
  • How /v1/snapshot pins a temporal coordinate as sugar over /v1/query
  • How /v1/compare reports added/removed/modified between two bitemporal coordinates, down to the per-property old and new value