# How to Implement Bi-Temporal Knowledge Graphs with Time Travel Queries in Semantica

> Implement bi-temporal knowledge graphs with time travel queries using Semantica. Learn to attach valid and transaction time axes for powerful point-in-time data analysis.

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

---

**Semantica implements bi-temporal knowledge graphs by attaching independent *valid-time* and *transaction-time* axes to every graph element, enabling point-in-time queries via `TemporalGraphQuery` and versioned snapshots via `TemporalVersionManager`.**

Bi-temporal knowledge graphs track *when facts were true in the real world* and *when they were recorded in the system*. This dual-axis approach is essential for audit trails, regulatory compliance, and historical analysis. The Semantica framework provides a complete implementation through three core modules: data modeling, query execution, and version management.

## Understanding the Bi-Temporal Data Model

Semantica's foundation is the `BiTemporalFact` dataclass defined in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py). Every relationship in the knowledge graph wraps four timestamp fields:

| Field | Axis | Purpose |
|-------|------|---------|
| `valid_from` | Valid-time | When the fact became true in reality |
| `valid_until` | Valid-time | When the fact ceased being true (`TemporalBound.OPEN` for ongoing) |
| `recorded_at` | Transaction-time | When the fact was first stored in the KG |
| `superseded_at` | Transaction-time | When a newer version replaced this record |

```python
@dataclass
class BiTemporalFact:
    valid_from: Optional[datetime]
    valid_until: Optional[datetime | TemporalBound]
    recorded_at: datetime = field(default_factory=_default_recorded_at)
    superseded_at: datetime | TemporalBound = TemporalBound.OPEN

```

The `BiTemporalFact.from_relationship()` factory method normalizes raw relationship dictionaries, parsing ISO strings and applying sensible defaults for omitted fields.

## Storing Bi-Temporal Facts

When adding relationships to your knowledge graph, supply any subset of the four temporal fields. The framework handles normalization automatically.

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

rel = {
    "source": "Alice",
    "target": "Bob",
    "type": "friend",
    "valid_from": "2022-01-01",
    # valid_until omitted → open-ended interval

    "recorded_at": "2022-03-15",
}
fact = BiTemporalFact.from_relationship(rel)

# Parsed result:

#   valid_from = 2022-01-01T00:00:00Z

#   valid_until = TemporalBound.OPEN

#   recorded_at = 2022-03-15T00:00:00Z

#   superseded_at = TemporalBound.OPEN

```

All parsing, serialization, and `TemporalBound.OPEN` handling resides in [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py). The framework accepts ISO 8601 strings, Unix timestamps, or native `datetime` objects interchangeably.

## Executing Time Travel Queries

The **time travel engine** is `TemporalGraphQuery` in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py). It reconstructs internally consistent graph views for any historical moment.

### Query by Valid-Time (Default)

Valid-time queries answer: *"What was true in the real world at this date?"*

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

tgq = TemporalGraphQuery()
snapshot = tgq.query_at_time(
    graph=my_kg,
    query="any",
    at_time="2023-06-10",
)
print(snapshot["relationships"])  # Only relationships valid on 2023-06-10

```

### Query by Transaction-Time

Transaction-time queries answer: *"What did the system record at this date?"*

```python
snapshot_tx = tgq.query_at_time(
    graph=my_kg,
    query="any",
    at_time="2023-01-01",
    time_axis="transaction",  # Switch to recording timeline

)

```

The `query_at_time` method delegates to `_relationship_active_at_time()` for interval overlap detection, using the `TemporalReasoningEngine` from [`temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_reasoning.py) to implement Allen's interval algebra.

## Managing Versioned Snapshots

For auditability and long-term storage, `TemporalVersionManager` (also in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) around line 1410) creates immutable, labeled snapshots.

### Creating and Persisting Versions

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

# In-memory storage (default)

manager = TemporalVersionManager()

# Persistent SQLite storage

manager = TemporalVersionManager(storage_path="kg_versions.db")

v1 = manager.create_version(
    graph=my_kg,
    version_label="v2023-01",
    timestamp="2023-01-01T00:00:00Z",
    metadata={"author": "alice"},
)

```

### Comparing Versions

```python
v2 = manager.create_version(
    graph=my_kg,
    version_label="v2023-06",
    timestamp="2023-06-01T00:00:00Z",
)

diff = manager.compare_versions(
    v1, v2,
    comparison_metrics=["added", "removed"]
)
print("Added:", diff["added"]["relationships"])
print("Removed entities:", diff["removed"]["entities"])

```

## Complete Implementation Example

This workflow demonstrates inserting bi-temporal data, querying historically, and versioning the results:

```python
from semantica.kg.temporal_model import BiTemporalFact
from semantica.kg.temporal_query import TemporalGraphQuery, TemporalVersionManager
from semantica.utils.graph import GraphBuilder

# 1. Build graph with bi-temporal relationships

graph = GraphBuilder().empty()
graph.add_relationship(
    source="Alice",
    target="ProjectX",
    type="works_on",
    valid_from="2021-05-01",
    valid_until="2022-12-31",
    recorded_at="2021-06-15",
)

# 2. Time-travel query on transaction axis

tgq = TemporalGraphQuery()
historic_view = tgq.query_at_time(
    graph=graph,
    query="any",
    at_time="2022-03-01",
    time_axis="transaction",
)

# 3. Create versioned snapshots

manager = TemporalVersionManager(storage_path="audit.db")
snap1 = manager.create_version(graph, "v1", "2022-01-01")

# ... modify graph ...

snap2 = manager.create_version(graph, "v2", "2023-01-01")

# 4. Analyze evolution

diff = manager.compare_versions(snap1, snap2)

```

## Architectural Overview

```

┌─────────────────────────────┐
│   Application Layer          │
│   (inserts, queries)         │
└─────────────┬───────────────┘
              │
┌─────────────▼───────────────┐
│  BiTemporalFact             │  ← semantica/kg/temporal_model.py
│  (normalizes timestamps)    │
└─────────────┬───────────────┘
              │
┌─────────────▼───────────────┐
│  TemporalGraphQuery         │  ← semantica/kg/temporal_query.py
│  (time-travel engine)       │
└─────────────┬───────────────┘
              │
┌─────────────▼───────────────┐
│  TemporalVersionManager     │  ← semantica/kg/temporal_query.py
│  (snapshots, diff, persist) │
└─────────────────────────────┘
                            │
┌───────────────────────────▼─┐
│  TemporalReasoningEngine    │  ← semantica/kg/temporal_reasoning.py
│  (Allen interval algebra)   │
└─────────────────────────────┘

```

The `TemporalReasoningEngine` handles all interval calculations including overlap detection, containment tests, and granularity-aware normalization.

## Key Source Files

| Component | File | Function |
|-----------|------|----------|
| Data model | [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) | `BiTemporalFact`, parsers, bounds |
| Query engine | [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) | `TemporalGraphQuery`, `TemporalVersionManager` |
| Interval algebra | [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) | `TemporalReasoningEngine`, `TemporalInterval` |
| Storage backends | [`semantica/change_management/managers.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/change_management/managers.py) | `InMemoryVersionStorage`, `SQLiteVersionStorage` |

## Summary

- **BiTemporalFact** in [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py) normalizes four timestamp fields for dual-axis tracking.
- **TemporalGraphQuery** in [`temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_query.py) executes point-in-time queries on either valid-time or transaction-time axes.
- **TemporalVersionManager** creates immutable, comparable, and persistable snapshots of graph state.
- All interval operations delegate to **TemporalReasoningEngine** implementing formal temporal logic.
- Combine these components to build audit-compliant knowledge graphs with full historical reconstruction capabilities.

## Frequently Asked Questions

### What is the difference between valid-time and transaction-time?

**Valid-time** records when a fact was true in the real world—such as an employee's tenure at a company. **Transaction-time** records when that fact was entered into the knowledge graph system. A fact can have different values on each axis: you might learn today (transaction-time) that someone worked somewhere last year (valid-time). Semantica stores both independently via `BiTemporalFact`.

### How do I query what the knowledge graph looked like last year?

Use `TemporalGraphQuery.query_at_time()` with your target date. For the real-world state, call with default parameters (valid-time axis). For the system state as recorded at that date, specify `time_axis="transaction"`. Both return filtered relationship lists containing only elements active at that moment.

### Can I store snapshots for compliance auditing?

Yes. `TemporalVersionManager` creates immutable, labeled snapshots with full metadata. Pass `storage_path` to persist versions in SQLite, or use the default in-memory store for temporary versioning. The `compare_versions()` method generates added/removed/changed reports suitable for audit trails.

### What happens if I omit temporal fields when adding relationships?

`BiTemporalFact.from_relationship()` applies sensible defaults: `TemporalBound.OPEN` for unspecified end dates (meaning the interval continues indefinitely), and `datetime.now(UTC)` for omitted `recorded_at` values. This ensures every relationship has a complete, queryable bi-temporal representation.