# How Semantica Handles Bi-Temporal Facts: Architecture and Implementation Guide

> Learn how Semantica handles bi-temporal facts, tracking real-world truth and system recording times with its BiTemporalFact dataclass. Explore the architecture and implementation.

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

---

**Semantica stores every relationship as a bi-temporal fact that tracks both valid time (when a fact is true in the real world) and transaction time (when it was recorded in the system), using the `BiTemporalFact` dataclass in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) to normalize timestamps and handle open-ended intervals.**

The open-source Semantica framework (semantica-agi/semantica) implements a robust bi-temporal data model that enables powerful time-travel queries and audit capabilities across its knowledge graph. Unlike standard graph databases that store only current state, Semantica treats time as a first-class citizen by persisting four distinct timestamps for every relationship.

## Understanding Valid Time vs. Transaction Time

Semantica’s bi-temporal facts maintain two independent orthogonal timelines, ensuring complete historical accuracy and auditability:

- **Valid time** represents when a fact is true in reality, controlled by the `valid_from` and `valid_until` fields. This allows the system to answer questions like "Who was the CEO on March 15, 2020?" regardless of when that information was entered into the database.
- **Transaction time** records the system’s history of changes via `recorded_at` (when the fact was first recorded) and `superseded_at` (when it was updated or replaced). This creates an immutable audit trail showing exactly when data corrections occurred.

According to the Semantica source code, the core implementation lives entirely within [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py), where the `BiTemporalFact` dataclass provides a thin wrapper around raw relationship dictionaries, centralizing all parsing, validation, and serialization logic to ensure consistency across the codebase.

## Core Implementation in [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py)

### The `BiTemporalFact` Dataclass

The foundation of bi-temporal handling is the `BiTemporalFact` dataclass, which exposes the four critical timestamp fields while normalizing heterogeneous input formats. When constructing facts, the system accepts ISO strings, epoch numbers, or native `datetime` objects, converting them internally to timezone-aware UTC timestamps.

The dataclass is designed for backward compatibility: existing code can continue reading and writing raw dictionary fields (`valid_from`, `valid_until`, `recorded_at`, `superseded_at`), while the wrapper guarantees consistent validation and parsing semantics.

### Handling Open-Ended Intervals with `TemporalBound`

Semantica uses a dedicated `TemporalBound` enum to represent unbounded time intervals. This enum contains a single sentinel value, `OPEN`, which the reasoning engine interprets as `datetime.max` for `valid_until` and `datetime.min` for `superseded_at`.

Using `TemporalBound.OPEN` rather than null values allows the query engine to perform deterministic range comparisons without special-case null handling, simplifying the implementation of temporal logic operators.

### Factory Methods and Serialization

The `BiTemporalFact.from_relationship(rel)` factory method constructs instances from plain relationship dictionaries, automatically filling defaults for missing transaction-time fields and assigning `TemporalBound.OPEN` to unspecified end dates. This method handles all timestamp normalization and validation during ingestion.

For persistence, the `to_relationship_fields()` method converts the internal dataclass back into a JSON-serializable dictionary, ensuring that all `datetime` objects render as ISO-8601 strings while mapping `TemporalBound.OPEN` back to `None` for compatibility with standard JSON stores.

## Creating and Serializing Bi-Temporal Facts

You can construct bi-temporal facts directly from relationship data using the factory method, which infers transaction timestamps when omitted:

```python
from semantica.kg import BiTemporalFact, TemporalBound

rel = {
    "source": "alice",
    "target": "acme_corp",
    "type": "ceo_of",
    "valid_from": "2018-01-01",
    "valid_until": "2022-06-01",
}

fact = BiTemporalFact.from_relationship(rel)

print(fact.valid_from)      # datetime(2018, 1, 1, tzinfo=UTC)

print(fact.valid_until)     # datetime(2022, 6, 1, tzinfo=UTC)

print(fact.recorded_at)     # Current UTC timestamp (auto-filled)

print(fact.superseded_at)   # TemporalBound.OPEN

```

To serialize the fact for storage or API response, convert it back to a plain dictionary with normalized string formats:

```python
fields = fact.to_relationship_fields()
print(fields["valid_from"])      # "2018-01-01T00:00:00Z"

print(fields["valid_until"])     # "2022-06-01T00:00:00Z"

print(fields["recorded_at"])     # "2024-09-08T12:34:56Z"

print(fields["superseded_at"])   # None (represents OPEN)

```

## Querying Bi-Temporal Data

### Point-in-Time Reconstruction with `TemporalGraphQuery`

The `TemporalGraphQuery` class leverages bi-temporal metadata to reconstruct knowledge graph snapshots as they existed at arbitrary moments in real-world history. This module filters relationships based on valid-time overlap with the query timestamp, returning only facts that were actually true during the specified period.

```python
from semantica.kg import GraphBuilder, TemporalGraphQuery

builder = GraphBuilder()
kg = builder.build(sources=[{"entities": [], "relationships": [rel]}])

query = TemporalGraphQuery(temporal_granularity="day")
snapshot = query.reconstruct_at_time(kg, "2020-03-15")
print(snapshot["relationships"])   # Only relationships valid on 2020-03-15

```

### Version Management with `TemporalVersionManager`

For audit and compliance requirements, `TemporalVersionManager` handles transaction-time versioning, creating immutable snapshots that preserve the complete history of when facts were recorded or superseded. This enables "as-of" queries against the transaction timeline, distinct from valid-time queries.

```python
from semantica.kg import TemporalVersionManager

versioner = TemporalVersionManager()
versioner.create_snapshot(
    kg,
    version_label="2024-Q1",
    author="alice@example.com",
    description="Quarter-1 snapshot after reorganisations",
)

for v in versioner.list_versions():
    print(v["label"], v["timestamp"])

```

## Integration with the Knowledge Graph Stack

The bi-temporal model integrates across multiple specialized modules:

- **[`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py)**: Implements `TemporalGraphQuery` for range queries, pattern detection, and temporal filtering using valid-time intervals.
- **[`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py)**: Provides Allen interval algebra operations over `TemporalInterval` objects derived from `BiTemporalFact` instances, enabling sophisticated temporal logic such as "before," "during," and "overlaps" relationships.
- **[`semantica/kg/temporal_version_manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_version_manager.py)**: Manages persistent versioned snapshots and supersession semantics using the transaction-time fields.

For complete API documentation, see [`docs/reference/temporal.md`](https://github.com/semantica-agi/semantica/blob/main/docs/reference/temporal.md) in the repository, which describes the bi-temporal query language and versioning strategies.

## Summary

- **Dual timeline tracking**: Semantica implements **bi-temporal facts** using four timestamp fields (`valid_from`, `valid_until`, `recorded_at`, `superseded_at`) to distinguish between real-world validity and system record history.
- **Centralized validation**: The `BiTemporalFact` dataclass in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) normalizes all timestamp formats and enforces consistent open-ended interval semantics via `TemporalBound.OPEN`.
- **Flexible querying**: `TemporalGraphQuery` reconstructs point-in-time valid-time snapshots, while `TemporalVersionManager` preserves transaction-time audit trails.
- **Composable architecture**: Bi-temporal metadata flows through the query engine, reasoning module (Allen algebra), and versioning system to enable complex temporal intelligence without breaking existing graph operations.

## Frequently Asked Questions

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

**Valid time** indicates when a fact was actually true in the real world (e.g., "Alice was CEO from 2018 to 2022"), while **transaction time** tracks when the system recorded or modified that fact (e.g., "We learned about Alice's tenure on January 15, 2018 and updated the record on March 3, 2022"). This separation allows Semantica to answer both "What did we know then?" and "What was true then?" questions independently.

### How does Semantica handle relationships with no known end date?

Semantica uses the `TemporalBound.OPEN` sentinel value defined in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) to represent unbounded valid-time or transaction-time intervals. When `valid_until` or `superseded_at` is set to `TemporalBound.OPEN`, the reasoning engine treats these as `datetime.max` or `datetime.min` respectively, ensuring that range comparisons succeed without special null handling.

### Can I query the knowledge graph as it appeared on a specific date in the past?

Yes. The `TemporalGraphQuery` class provides the `reconstruct_at_time()` method, which takes a knowledge graph and a target date string, then returns a snapshot containing only relationships whose valid-time intervals overlap with that date. This enables point-in-time analysis of historical graph states without maintaining separate database snapshots.

### Is the bi-temporal model backward compatible with standard graph operations?

Yes. According to the Semantica source code, the `BiTemporalFact` wrapper is designed for **backward compatibility**: existing code can continue reading and writing raw relationship dictionaries containing `valid_from`, `valid_until`, and other temporal fields. The wrapper only enforces validation and normalization when explicitly invoked via `from_relationship()` or `to_relationship_fields()`.