# What Is a Bi-Temporal Fact in Semantica? Temporal Knowledge Graph Modeling Explained

> Discover bi-temporal facts in Semantica AGI. Learn how this wrapper adds time dimensions to knowledge graphs for point-in-time queries and versioning.

- Repository: [Semantica /semantica](https://github.com/semantica-agi/semantica)
- Tags: deep-dive
- Published: 2026-09-10

---

**A bi-temporal fact is a backward-compatible wrapper that adds valid-time and transaction-time dimensions to knowledge graph relationships, enabling point-in-time queries and versioning without breaking legacy code.**

In the `semantica-agi/semantica` repository, every relationship in the knowledge graph can be enriched with temporal metadata through the **bi-temporal fact** abstraction. This pattern captures when a fact is true in the real world (valid time) and when it was recorded in the system (transaction time), providing a robust foundation for temporal reasoning and historical analysis.

## What Is a Bi-Temporal Fact?

According to the Semantica source code in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py), a **bi-temporal fact** is a specialized wrapper around a plain relationship dictionary. The class `BiTemporalFact` (lines 27-41) injects four temporal fields into every edge, allowing the knowledge graph to track *when* a statement is true independently from *when* it was stored.

The implementation maintains **backward compatibility**—the wrapper can be created from an existing relationship dict via `from_relationship()` and can flatten back into a dict via `to_relationship_fields()`, ensuring legacy code that reads or writes `valid_from` or `valid_until` continues to function unchanged.

## Core Temporal Fields

A bi-temporal fact tracks two distinct timelines through four fields:

- **valid_from** (`datetime | None`): The *valid-time* when the fact becomes true in the modeled world.
- **valid_until** (`datetime | TemporalBound | None`): The *valid-time* when the fact stops being true. Uses the sentinel `TemporalBound.OPEN` for open-ended validity.
- **recorded_at** (`datetime`): The *transaction-time* when the fact was written to the graph. Auto-populated with current UTC time if omitted.
- **superseded_at** (`datetime | TemporalBound`): The *transaction-time* when the fact was logically removed or updated. Also uses `TemporalBound.OPEN` for active facts.

This dual-axis model (valid vs. transaction time) enables **point-in-time** snapshots and audit trails without mutating historical records.

## How Semantica Uses Bi-Temporal Facts

### 1. Normalization of Incoming Relationships

When a relationship is ingested from extraction pipelines, the function `deserialize_relationship_temporal_fields` calls `BiTemporalFact.from_relationship()` (lines 36-44 in [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py)). This guarantees that all four temporal fields are present and correctly typed before the edge enters the knowledge graph.

### 2. Temporal Query Engine

The query engine retrieves valid and transaction intervals via helpers defined in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) (lines 48-55). These utilities build a `BiTemporalFact` instance and return the requested axis—either `valid` or `transaction`—enabling time-slice queries like "What was true on March 15, 2023?"

### 3. Temporal Reasoning

Reasoning algorithms in [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) (lines 46-50) coerce raw facts into `BiTemporalFact` objects before extracting validity intervals. This ensures that causal ordering and pattern detection operate on consistently typed temporal bounds.

### 4. Versioning and Provenance

The `TemporalVersionManager` (referenced in the temporal model implementation) creates new `BiTemporalFact` instances with fresh `recorded_at` timestamps to store graph snapshots. This supports historical provenance tracking and rollback capabilities.

### 5. Serialization

When exporting relationships to JSON or RDF, the helper `relationship_to_json_ready` (lines 47-55 in [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py)) serializes temporal fields using ISO-8601 format, ensuring consistent representation across external systems.

## Working with Bi-Temporal Facts: Code Examples

### Creating a Bi-Temporal Fact from Raw Data

```python
from semantica.kg import BiTemporalFact

raw = {
    "subject": "Alice",
    "predicate": "employed_at",
    "object": "AcmeCorp",
    "valid_from": "2022-01-01",
    "valid_until": None,  # open-ended valid time

    # transaction-time fields are optional

}

fact = BiTemporalFact.from_relationship(raw)

print(fact.valid_from)      # 2022-01-01T00:00:00+00:00 (datetime)

print(fact.valid_until)     # TemporalBound.OPEN

print(fact.recorded_at)     # auto-populated current UTC datetime

print(fact.superseded_at)   # TemporalBound.OPEN

```

### Converting Back to Dictionary Format

```python

# Flatten for storage or export

relationship_ready = fact.to_relationship_fields()

# Result contains ISO-8601 strings:

# {

#   "valid_from": "2022-01-01T00:00:00Z",

#   "valid_until": None,

#   "recorded_at": "2024-09-10T12:34:56Z",

#   "superseded_at": None,

# }

```

### Using in Temporal Queries

```python
from datetime import datetime, timezone
from semantica.kg import TemporalVersionManager

# Assume kg is a loaded knowledge graph

tvm = TemporalVersionManager(kg)

# Find relationships valid on a specific date

target_date = datetime(2023, 3, 15, tzinfo=timezone.utc)

snapshot = tvm.snapshot_at(timestamp="2023-03-15")
valid_edges = [
    e for e in snapshot["relationships"]
    if BiTemporalFact.from_relationship(e).valid_from <= target_date <
       (BiTemporalFact.from_relationship(e).valid_until or datetime.max.replace(tzinfo=timezone.utc))
]

```

## Key Implementation Files

| File | Role |
|------|------|
| [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) | Defines `BiTemporalFact`, parsers, and serializers (lines 27-55). |
| [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) | Query engine helpers extracting valid/transaction bounds. |
| [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) | Reasoning algorithms coercing facts into bi-temporal objects. |
| [`semantica/kg/__init__.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/__init__.py) | Re-exports `BiTemporalFact` for the public API. |
| [`tests/test_395_temporal_semantics_comprehensive.py`](https://github.com/semantica-agi/semantica/blob/main/tests/test_395_temporal_semantics_comprehensive.py) | Unit tests verifying construction and conversion. |
| [`tests/semantic_extract/test_temporal_extraction.py`](https://github.com/semantica-agi/semantica/blob/main/tests/semantic_extract/test_temporal_extraction.py) | Integration tests for extraction → normalization pipeline. |

## Summary

- A **bi-temporal fact** is the canonical representation of a time-stamped edge in Semantica, unifying valid-time and transaction-time axes.
- The `BiTemporalFact` class in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) provides **backward-compatible** wrapping via `from_relationship()` and `to_relationship_fields()`.
- Four temporal fields (`valid_from`, `valid_until`, `recorded_at`, `superseded_at`) enable **point-in-time** queries and audit trails.
- All higher-level temporal features—querying, reasoning, versioning—depend on this unified model.

## Frequently Asked Questions

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

**Valid time** refers to when a fact is true in the real world (captured by `valid_from` and `valid_until`), while **transaction time** records when the fact was stored in the knowledge graph (captured by `recorded_at` and `superseded_at`). This distinction allows Semantica to answer both "What was true last year?" and "When did we learn about it?"

### How does BiTemporalFact maintain backward compatibility with legacy relationship dictionaries?

The class provides bidirectional conversion methods: `from_relationship()` ingests plain dictionaries and populates missing temporal fields with defaults, while `to_relationship_fields()` exports the temporal data back to dictionary format. Legacy code reading `valid_from` or `valid_until` continues to work because these fields remain standard dictionary keys in the output.

### What is TemporalBound.OPEN and when is it used?

`TemporalBound.OPEN` is a sentinel value indicating that a time interval has no defined end. It is used for `valid_until` when a fact is currently true, and for `superseded_at` when a fact remains active in the latest transaction time. This avoids null ambiguity in temporal comparisons.

### Which Semantica modules handle bi-temporal fact serialization?

Serialization logic resides primarily in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py), specifically in `relationship_to_json_ready` and related helpers (lines 47-55). The `TemporalVersionManager` utilizes these serializers when creating version snapshots, ensuring ISO-8601 compliant output for downstream consumers.