# Bi-Temporal Facts and Time Travel Queries in Semantica: A Complete Guide

> Explore bi-temporal facts and time travel queries in Semantica. Learn how to track relationship validity and system records to reconstruct historical knowledge graph states.

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

---

**Semantica implements bi-temporal modeling to track both when relationships existed in reality (valid time) and when they were recorded in the system (transaction time), enabling time-travel queries that reconstruct historical knowledge graph states at any specific moment.**

Semantica is an open-source knowledge graph framework that treats temporal data as a first-class citizen. By modeling every relationship as a **bi-temporal fact**, the system supports sophisticated **time-travel queries** that allow developers to audit historical changes and reason about world states at specific moments without requiring external temporal databases. This architecture is implemented entirely within Python's data model, with core logic residing in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) and [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py).

## Understanding Bi-Temporal Facts in Semantica

A **bi-temporal fact** is a relationship that carries two independent time dimensions, allowing the system to distinguish between reality and record-keeping.

### Valid Time vs. Transaction Time

According to the Semantica source code, each relationship stores four critical timestamps managed by the `BiTemporalFact` class in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) (lines 27-44):

- **Valid time** (`valid_from`, `valid_until`): Represents when the relationship was true in the real world. Uses `TemporalBound.OPEN` to indicate ongoing relationships without an end date.
- **Transaction time** (`recorded_at`, `superseded_at`): Tracks when the fact was entered into or replaced within the graph database, essential for audit trails and compliance.

These dimensions are orthogonal—a fact may have been recorded yesterday about a relationship that started five years ago.

### The BiTemporalFact Data Model

The `BiTemporalFact` class acts as a thin wrapper around plain relationship dictionaries, providing helpers for parsing, normalizing, and serializing temporal fields. When ingesting data, `deserialize_relationship_temporal_fields` converts raw JSON or dictionary inputs into properly typed `BiTemporalFact` instances, handling ISO date parsing and `OPEN` sentinel values automatically.

The class ensures backward compatibility by storing data in standard relationship dictionaries while adding temporal semantics, allowing existing graph operations to function unchanged while enabling temporal queries when needed.

## How Time Travel Queries Work in Semantica

Time-travel queries allow you to ask "what did the graph look like on March 15, 2023?" or "how did this relationship evolve over the last year?" The query engine filters the entire knowledge graph to return a self-consistent sub-graph representing a specific temporal slice.

### The Temporal Query Engine Architecture

The `TemporalGraphQuery` class in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) serves as the primary interface for temporal operations. According to the source analysis (lines 41-78), the engine supports three query modes:

1. **Point-in-time queries** using `query_at_time()` to snapshot the graph at a specific instant
2. **Range queries** using `query_time_range()` to extract subgraphs valid across an interval
3. **Evolution analysis** using `analyze_evolution()` to track how relationships change over time

### Reconstructing Historical Graph States

The core reconstruction logic lives in the `reconstruct_at_time` method. This method implements the following algorithm:

- Parses the requested timestamp using `_parse_time()` to handle ISO strings, datetime objects, or Unix timestamps uniformly
- Iterates through all relationships in the graph's `"relationships"` list
- Extracts temporal bounds via `_get_axis_bounds()`, selecting either valid time, transaction time, or both axes depending on query parameters
- Filters active relationships using `_relationship_active_at_time()`, which checks if the query time falls within the fact's temporal bounds
- Constructs a new graph dictionary containing only entities referenced by the filtered relationships

This approach creates immutable snapshots without modifying the underlying graph storage, enabling safe historical analysis and "as-of" reporting.

## Practical Implementation Examples

### Creating Bi-Temporal Facts with BiTemporalFact

When ingesting relationships, use the `BiTemporalFact` class to ensure proper temporal encoding:

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

relationship = {
    "source": "person:alice",
    "target": "company:acme",
    "type": "employed_as",
    "valid_from": "2021-01-01",
    "valid_until": "2023-06-30",
    # Transaction timestamps are optional

}

fact = BiTemporalFact.from_relationship(relationship)
print(fact.to_relationship_fields())

# Output includes normalized ISO timestamps:

# {

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

#   "valid_until": "2023-06-30T00:00:00Z",

#   "recorded_at": "2021-01-01T12:34:56Z",  # auto-generated

#   "superseded_at": None

# }

```

The `from_relationship()` factory method handles the conversion, while `to_relationship_fields()` serializes the fact back to dictionary form for storage in the graph's relationship list.

### Executing Point-in-Time Queries

To retrieve a historical snapshot of your knowledge graph, instantiate `TemporalGraphQuery` and specify your target time:

```python
from semantica.kg.temporal_query import TemporalGraphQuery

# Assume `graph` is a dict with "entities" and "relationships" keys

engine = TemporalGraphQuery()

snapshot = engine.query_at_time(
    graph,
    query="any",               # placeholder for future query filtering

    at_time="2022-04-15",      # Accepts ISO strings, datetime objects, or timestamps

)

print(f"Entities: {snapshot['num_entities']}")
print(f"Relationships: {snapshot['num_relationships']}")

# Results include only relationships where valid_from ≤ 2022-04-15 ≤ valid_until

```

For transaction-time auditing (seeing what the system knew at a specific time), the engine adjusts `_get_axis_bounds()` to examine `recorded_at` and `superseded_at` fields instead of valid-time bounds.

### Analyzing Relationship Evolution

The `analyze_evolution` method provides metrics about how relationships change across time windows:

```python
evolution = engine.analyze_evolution(
    graph,
    relationship="employed_as",
    start_time="2020-01-01",
    end_time="2024-01-01",
    metrics=["count", "diversity", "stability"],
)

print(evolution["stability"])  # Average duration in seconds that relationships remained valid

```

This leverages the interval algebra from [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) (specifically `TemporalInterval` and `IntervalRelation`) to calculate overlaps and gaps in relationship histories.

## Supporting Infrastructure and Related Components

Beyond the core temporal model and query engine, Semantica provides additional infrastructure for bi-temporal operations:

- **[`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py)**: Implements the underlying interval algebra (`TemporalInterval`, `IntervalRelation`) used for reasoning about temporal overlaps and containment relationships
- **[`semantica/visualization/temporal_visualizer.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/visualization/temporal_visualizer.py)**: Renders bi-temporal data and query results, allowing visual inspection of how the graph changes over time
- **[`semantica/change_management/managers.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/change_management/managers.py)**: Contains `TemporalVersionManager`, which persists graph snapshots using the same bi-temporal facts, enabling version rollback and permanent audit trails

These components work together to provide a complete bi-temporal knowledge graph solution without external dependencies.

## Summary

- **Bi-temporal facts** capture both valid time (real-world truth) and transaction time (system record) using the `BiTemporalFact` class in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py)
- **Time-travel queries** reconstruct historical graph states via `TemporalGraphQuery.reconstruct_at_time()` in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py)
- **Dual-axis filtering** allows queries against valid time, transaction time, or both simultaneously for flexible historical analysis
- **Pure Python implementation** avoids external temporal database dependencies while supporting complex interval algebra through [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py)

## Frequently Asked Questions

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

**Valid time** represents when a relationship actually existed in the real world (e.g., Alice worked at Acme from 2020 to 2022), stored in `valid_from` and `valid_until` fields. **Transaction time** tracks when the system recorded or updated that fact (e.g., the data was entered on March 1, 2023), stored in `recorded_at` and `superseded_at` fields. This separation allows you to discover both what was true historically and when the system knew about it.

### How does Semantica handle open-ended temporal intervals?

The framework uses `TemporalBound.OPEN` as a sentinel value to represent unbounded intervals. When `valid_until` is `None` or `TemporalBound.OPEN`, the relationship is considered currently active for valid-time queries. Similarly, when `superseded_at` is open, the fact represents the current system state for transaction-time queries. The `_relationship_active_at_time()` method in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) explicitly handles these open bounds during filtering.

### Can I query using both temporal dimensions simultaneously?

Yes. The `TemporalGraphQuery` engine supports querying along either axis independently or both together. The `_get_axis_bounds()` method extracts the appropriate temporal bounds based on query parameters, allowing you to ask questions like "What did we know yesterday about relationships that were valid last year?" This bi-temporal capability ensures complete auditability and historical accuracy.

### What file contains the main query logic for temporal operations?

The primary temporal query implementation resides in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py), which defines the `TemporalGraphQuery` class containing methods like `query_at_time()`, `reconstruct_at_time()`, and `analyze_evolution()`. The data model definitions, including `BiTemporalFact`, are located in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py).