# How to Implement Bi-Temporal Facts with BiTemporalFact for Time-Travel Queries in Semantica

> Implement bi-temporal facts in Semantica using BiTemporalFact for immutable historical auditing and powerful time-travel queries. Explore TemporalGraphQuery for advanced data analysis.

- Repository: [Semantica /semantica](https://github.com/semantica-agi/semantica)
- Tags: how-to-guide
- Published: 2026-08-09

---

**Use the `BiTemporalFact` class in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) to capture both valid time and transaction time, enabling immutable historical auditing and time-travel queries via `TemporalGraphQuery`.**

The Semantica knowledge graph framework supports audit-ready temporal reasoning through bi-temporal data modeling. By tracking when facts are true in reality (**valid time**) and when they are recorded in the system (**transaction time**), you can execute precise time-travel queries against any historical state of the graph. The core implementation resides in the `semantica-agi/semantica` repository, specifically within the temporal knowledge graph modules.

## Understanding Bi-Temporal Fact Structure

A `BiTemporalFact` consists of six core components defined in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py):

- **entity** – The subject node identifier (e.g., `"Person:123"`).
- **relationship** – The predicate or edge name (e.g., `"employedAt"`).
- **value** – The object node or literal value.
- **valid_from / valid_to** – `TemporalBound` objects describing when the fact is true in the real world.
- **transaction_from / transaction_to** – `TemporalBound` objects describing when the fact exists in the database.

`TemporalBound` is a lightweight wrapper that accepts either a concrete `datetime` object or the special markers `OPEN` and `CLOSED` to represent unbounded intervals. Helper functions `parse_temporal_bound` and `parse_temporal_value` normalize ISO-8601 strings, epoch milliseconds, or custom period specifications into these bound objects.

## Constructing Bi-Temporal Facts

The preferred factory method is `BiTemporalFact.from_relationship()`, which accepts a dictionary matching the canonical relationship representation used throughout Semantica’s pipelines:

```python
from semantica.kg import BiTemporalFact

fact = BiTemporalFact.from_relationship({
    "entity": "Person:123",
    "relationship": "employedAt",
    "value": "Company:XYZ",
    "valid_from": "2020-01-01",
    "valid_to": "2022-12-31",
    "transaction_from": "2020-02-01T10:00:00Z",
    "transaction_to": "OPEN"
})

```

Internally, this method normalizes the supplied string values, instantiates `TemporalBound` objects for each bound, and returns a fully-typed, immutable `BiTemporalFact` instance. This construction pattern is validated in the comprehensive test suite, particularly in [`test_395_temporal_semantics_comprehensive.py`](https://github.com/semantica-agi/semantica/blob/main/test_395_temporal_semantics_comprehensive.py) and [`semantic_extract/test_temporal_extraction.py`](https://github.com/semantica-agi/semantica/blob/main/semantic_extract/test_temporal_extraction.py).

## Executing Time-Travel Queries

Semantica’s query engine relies on three abstractions to filter historical states:

- **TemporalGraphQuery** – A query object that filters on valid and/or transaction intervals.
- **TemporalInterval** – Utility class computing overlaps between bounds.
- **TemporalReasoning** – Functions coercing inputs into `BiTemporalFact` objects and applying interval logic.

To query the graph "as of" a specific transaction timestamp, instantiate `TemporalGraphQuery` with the `as_of_transaction` parameter:

```python
from semantica.kg import TemporalGraphQuery

query = TemporalGraphQuery(
    entity="Person:123",
    as_of_transaction="2021-06-01T00:00:00Z"
)

# Returns only facts where transaction_from <= as_of_transaction < transaction_to

facts = graph.search(query)

```

The engine validates each `BiTemporalFact`’s transaction bounds using `TemporalInterval` utilities, ensuring that only the active revision at the requested time is returned.

## Managing Fact Revisions

Semantica implements an **immutable-by-design** versioning pattern. Rather than mutating existing facts, the system creates new `BiTemporalFact` instances with fresh `transaction_from` timestamps while automatically closing the prior fact’s `transaction_to` bound.

The revision logic resides in [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py), specifically within the `apply_revision` helper (approximately lines 180–210). This function uses `_coerce_fact` to normalize input data, then adjusts the old fact’s `transaction_to` to the new transaction’s start time before persisting the new fact. This guarantees complete auditability and enables reliable replay of the graph at any past point.

```python
from semantica.kg import BiTemporalFact

# Existing fact (retrieved from storage)

old_fact = kg.get_fact(entity="Person:42", relationship="livesIn")

# Define new revision

new_fact = BiTemporalFact.from_relationship({
    "entity": "Person:42",
    "relationship": "livesIn",
    "value": "City:Berlin",
    "valid_from": "2023-06-01",
    "valid_to": "OPEN",
    "transaction_from": "2023-06-01T09:30:00Z",
    "transaction_to": "OPEN"
})

# Backend handles closing old_fact.transaction_to automatically

kg.update_fact(old_fact, new_fact)

```

## Summary

- **BiTemporalFact** captures both valid time (real-world truth) and transaction time (system record) using `TemporalBound` objects defined in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py).
- Use `BiTemporalFact.from_relationship()` to construct facts from dictionary representations, with parsing handled by `parse_temporal_bound`.
- Execute historical queries via `TemporalGraphQuery` filtered by `as_of_transaction`, leveraging `TemporalInterval` for bound comparison.
- Fact updates create immutable revisions; `apply_revision` in [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) manages transaction interval closure automatically.

## Frequently Asked Questions

### What is the difference between valid time and transaction time in Semantica?

**Valid time** represents when a fact is true in the real world (e.g., an employee actually worked at a company from 2020 to 2022), while **transaction time** represents when the fact was recorded in the knowledge graph (e.g., the system learned about this employment on February 1, 2020). `BiTemporalFact` stores both as separate `TemporalBound` pairs to enable independent querying of historical reality versus historical database states.

### How do I query the state of the graph at a specific point in history?

Instantiate `TemporalGraphQuery` with the `as_of_transaction` parameter set to the desired timestamp. The query engine filters `BiTemporalFact` instances where the transaction interval contains the specified time, effectively returning the graph state as it existed at that moment. This is implemented in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py).

### Can I update an existing BiTemporalFact, or is it immutable?

`BiTemporalFact` objects are immutable. To "update" a fact, you insert a new `BiTemporalFact` with an updated `transaction_from` timestamp, while the system automatically sets the previous fact’s `transaction_to` to the new transaction’s start time. This append-only pattern, handled by `apply_revision` in [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py), preserves complete audit trails.

### What temporal formats does Semantica support for bound parsing?

The `parse_temporal_bound` and `parse_temporal_value` functions in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) accept ISO-8601 formatted strings (e.g., `"2023-01-01T00:00:00Z"`), Unix epoch milliseconds, or the special string literals `"OPEN"` and `"CLOSED"` for unbounded intervals. These parsers normalize all inputs into consistent `TemporalBound` objects.