-
Notifications
You must be signed in to change notification settings - Fork 8
Cookbook 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 →
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 setsvalid-fromto 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 →
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 →
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-toleaves the fact open-ended (valid forever fromvalid-from) - The default query filter (no
:valid-at) returns facts wherevalid-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 →
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:
-
retractmarks the fact as no longer asserted; it does not delete it from history - The re-asserted fact carries the same
valid-fromas 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 →
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(sincevalid-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-timeand filter[(> ?vf <now-ms>)]using:db/valid-from
Visualize: Open this recipe in the time travel visualizer →
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-timeto see all periods including non-overlapping historical ones
Visualize: Open this recipe in the time travel visualizer →
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-toto 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-fromso 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 →
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-fromin 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-oflets 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