Skip to content

Cookbook Bitemporal Modeling

Claude edited this page Sep 27, 2026 · 3 revisions

Bitemporal Modeling

Recipes for structuring data to take full advantage of Minigraf's bi-temporal model. These are the "write side" — how to assert facts — complementary to the query patterns in Audit and Time-Travel Idioms.

← Time-Travel Idioms | Application Workflows →


Recipe 1 — Point-in-time facts (default valid-time)

Problem: Assert a fact that becomes true "now" with no modeled expiry.

;; Email address recorded at the moment of the transaction
(transact [[:alice :person/email "alice@example.com"]])

;; valid-from = tx_id (wall-clock time of the transaction, Unix ms)
;; valid-to   = 9223372036854775807 (i64::MAX — open-ended)

Notes:

  • No {:valid-from ...} map needed; Minigraf sets valid-from to the transaction's Unix ms timestamp automatically
  • The default query filter (no :valid-at) returns these facts as long as they have not been retracted
  • Use this for simple current-state storage: contact info, settings, labels — anything that becomes true "now"

Visualize: Open this recipe in the time travel visualizer →


Recipe 2 — Bounded facts (known start and end)

Problem: Assert a fact that is only true for a specific, fully known time period.

;; Annual contract active for all of 2024
(transact {:valid-from "2024-01-01" :valid-to "2024-12-31"}
          [[:contract-99 :contract/status :active]
           [:contract-99 :contract/holder :alice]])

;; Per-fact valid-time map for mixed periods in one transact
(transact [[:policy-a :policy/status :active {:valid-from "2024-01-01" :valid-to "2024-06-30"}]
           [:policy-b :policy/status :active {:valid-from "2024-03-01" :valid-to "2024-12-31"}]])
;; [entity attribute value {:valid-from ... :valid-to ...}]

Notes:

  • Transaction-level {:valid-from ... :valid-to ...} applies to all facts in the batch
  • A per-fact {:valid-from ... :valid-to ...} map (the optional 4th element) overrides the transaction-level range for that fact
  • Bounded facts model contracts, subscriptions, approvals, and any period with a known start and end

Visualize: Open this recipe in the time travel visualizer →


Recipe 3 — Open-ended current facts (known start, no end)

Problem: Assert a fact that started on a known date and is still true today.

;; Employment beginning on a specific date, no known end date
(transact {:valid-from "2023-06-01"}
          [[:alice :works-at :startupco]])
;; valid-from = 2023-06-01; valid-to = i64::MAX (open-ended)

;; Query: who currently works at :startupco?
(query [:find ?person
        :where [?person :works-at :startupco]])
;; Returns :alice — the default filter sees open-ended facts as currently valid ✓

Notes:

  • Omitting :valid-to leaves the fact open-ended (valid forever from valid-from)
  • The default query filter (no :valid-at) returns facts where valid-from ≤ now; an open-ended fact set in the past is visible
  • To close this fact when the situation changes, use Recipe 7

Visualize: Open this recipe in the time travel visualizer →


Recipe 4 — Retroactive correction

Problem: Correct a fact that was recorded with a wrong value, while preserving the original erroneous record in history.

;; Wrong salary was recorded
(transact {:valid-from "2024-01-01"}
          [[:alice :salary 75000]])  ;; tx 1 — incorrect

;; Correction: Alice's actual salary was 80000 from the same date
(retract [[:alice :salary 75000]])   ;; tx 2 — marks old value as retracted
(transact {:valid-from "2024-01-01"}
          [[:alice :salary 80000]])  ;; tx 3 — correct value, same valid-from

;; Current state: corrected salary
(query [:find ?s :where [:alice :salary ?s]])
;; => 80000

;; History preserved: original wrong value visible at tx 1
(query [:find ?s
        :as-of 1
        :valid-at :any-valid-time
        :where [:alice :salary ?s]])
;; => 75000

Notes:

  • retract marks the fact as no longer asserted; it does not delete it from history
  • The re-asserted fact carries the same valid-from as the original — this is what makes it retroactive
  • The full audit trail (wrong value at tx 1, correction at tx 3) is always preserved and queryable

Visualize: Open this recipe in the time travel visualizer →


Recipe 5 — Future-dated assertion

Problem: Record a change that takes effect at a future date.

;; Promotion effective 2026-01-01, recorded today
(transact {:valid-from "2026-01-01"}
          [[:alice :title :senior-engineer]])

;; Query today (before 2026-01-01): title is not yet visible
(query [:find ?title :where [:alice :title ?title]])
;; => empty

;; Query at or after 2026-01-01
(query [:find ?title
        :valid-at "2026-01-01"
        :where [:alice :title ?title]])
;; => :senior-engineer

Notes:

  • The fact is stored immediately but invisible to queries without an explicit :valid-at (since valid-from > now)
  • Useful for scheduling promotions, pre-approvals, contract renewals, and "effective date" workflows
  • To list all future-dated facts: query with :valid-at :any-valid-time and filter [(> ?vf <now-ms>)] using :db/valid-from

Visualize: Open this recipe in the time travel visualizer →


Recipe 6 — Modeling overlapping valid periods

Problem: Represent a situation where an entity holds two concurrent values for the same attribute.

;; Alice holds two roles simultaneously during 2024
(transact {:valid-from "2024-01-01" :valid-to "2024-06-30"}
          [[:alice :role :project-lead]])
(transact {:valid-from "2024-03-01"}
          [[:alice :role :technical-advisor]])

;; Query on 2024-04-15: both roles are active
(query [:find ?role
        :valid-at "2024-04-15"
        :where [:alice :role ?role]])
;; => :project-lead, :technical-advisor

Notes:

  • Minigraf places no uniqueness constraint on attribute values across valid-time ranges — overlapping periods are fully supported
  • To enforce "at most one active value at a time", validate at the application layer before transacting
  • Use :valid-at :any-valid-time to see all periods including non-overlapping historical ones

Visualize: Open this recipe in the time travel visualizer →


Recipe 7 — Closing an open-ended fact

Problem: Record the real-world end of a situation that was modeled as open-ended.

;; Alice left StartupCo on 2025-12-31
;; Step 1: retract the open-ended fact
(retract [[:alice :works-at :startupco]])

;; Step 2: re-assert with an explicit valid-to
(transact {:valid-from "2023-06-01" :valid-to "2025-12-31"}
          [[:alice :works-at :startupco]])

Notes:

  • Minigraf facts are immutable; you cannot add valid-to to an existing open-ended fact in place
  • Retract + re-assert is the correct pattern; the original open-ended fact is preserved in history (visible via :as-of <tx before retraction>)
  • The re-asserted fact carries the original valid-from so the full employment period (2023-06-01 to 2025-12-31) is correctly modeled
  • Follow immediately with a new open-ended fact if Alice starts a new role: (transact {:valid-from "2026-01-15"} [[:alice :works-at :nextcorp]])

Visualize: Open this recipe in the time travel visualizer →


Recipe 8 — Correction vs. lifecycle end

Problem: Know when to use error correction (Recipe 4) vs. valid-time bounding (Recipe 7).

;; ERROR CORRECTION — the original value was WRONG
;; Preserve the original valid-from in the replacement
(retract [[:alice :salary 75000]])
(transact {:valid-from "2024-01-01"}  ;; same start date as the wrong fact
          [[:alice :salary 80000]])

;; LIFECYCLE END — the original value was CORRECT but the situation changed
;; Close the fact at the real-world end date; add a new fact for the next period
(retract [[:alice :works-at :startupco]])
(transact {:valid-from "2023-06-01" :valid-to "2025-12-31"}
          [[:alice :works-at :startupco]])
(transact {:valid-from "2026-01-15"}
          [[:alice :works-at :nextcorp]])

Notes:

  • The distinction is semantic, not mechanical — both use retract + re-assert
  • Error correction: preserve the original valid-from in the replacement; the wrong value was never correct in the real world
  • Lifecycle end: close the fact at the real-world end date; assert a new fact for the new period
  • Both preserve the full history of what was recorded and when; :as-of lets you see the database state before either change

Visualize: Open this recipe in the time travel visualizer →


← Time-Travel Idioms | Application Workflows →

Reference: Bi-temporal queries, Transact, Retract

Clone this wiki locally