# How Semantica's Bi-Temporal Graph Model Manages Valid Time vs. Recorded Time

> Discover how Semantica's bi-temporal graph model separates valid time from recorded time using four timestamps for accurate data management and historical tracking.

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

---

**Semantica stores every knowledge-graph relationship as a bi-temporal fact with four timestamps: `valid_from`/`valid_until` for real-world truth intervals and `recorded_at`/`superseded_at` for system-ingestion history.**

The Semantica AGI framework implements a robust bi-temporal data model that independently tracks when facts are true in reality versus when they were recorded in the system. This architecture enables complete historical reconstruction of both domain state and system knowledge evolution.

---

## What Is Bi-Temporal Modeling?

Bi-temporal modeling separates two distinct temporal dimensions for every fact in the knowledge graph:

| Timeline | Purpose | Fields |
|----------|---------|--------|
| **Valid time** | When the fact is true in the real world | `valid_from`, `valid_until` |
| **Transaction time** (recorded time) | When the fact was entered or modified in the system | `recorded_at`, `superseded_at` |

Each relationship in Semantica carries both timelines, allowing queries to reconstruct the graph as it was known at any system time, or as it truly existed at any world time.

---

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

The `BiTemporalFact` dataclass in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) encapsulates both timelines with automatic defaults and conversion utilities.

```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",
    # recorded_at omitted → auto-stamped at ingestion

}
fact = BiTemporalFact.from_relationship(rel)

print(fact.valid_from)   # 2018-01-01 00:00:00+00:00

print(fact.recorded_at)  # e.g. 2024-03-05 12:34:56+00:00 (auto-stamp)

```

Key implementation details from the source code:

- **`TemporalBound.OPEN`** is the sentinel for unbounded intervals (ongoing facts with no `valid_until` or `superseded_at`)
- **`_default_recorded_at`** provides fallback logic: uses `valid_from` if present, otherwise current UTC time
- **Factory method `from_relationship`** handles ingestion-time stamping and boundary parsing
- **`to_relationship_fields`** serializes back to storage-compatible format with ISO8601 strings

The file also contains helper functions `parse_temporal_value`, `parse_temporal_bound`, `serialize_temporal_value`, and `serialize_temporal_bound` for consistent temporal handling across the codebase.

---

## Querying by Valid Time vs. Recorded Time

Semantica provides distinct query mechanisms for each timeline, implemented across [`temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_query.py) and [`provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/provenance/manager.py).

### Valid-Time Queries: "What Was True Then?"

```python
from semantica.kg import TemporalGraphQuery

query = TemporalGraphQuery(temporal_granularity="day")
snapshot = query.reconstruct_at_time(kg, at_time="2020-05-01")

# Returns edges where valid_from ≤ 2020-05-01 < valid_until

```

The `TemporalGraphQuery.query_at_time` method filters on `valid_from` and `valid_until` to return the domain state as of a specific real-world date. This supports historical reasoning with Allen interval algebra (implemented in [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py)).

### Transaction-Time Queries: "What Did We Know Then?"

```python
from semantica.provenance.manager import ProvenanceManager

mgr = ProvenanceManager(storage=my_storage)
history = mgr.revision_history(fact_id="rel-123")

# Each entry: version, valid_from, valid_until, recorded_at, author

```

`ProvenanceManager.query_recorded_between` and `TemporalVersionManager.revision_history` filter on `recorded_at` and `superseded_at` to reconstruct the system's knowledge state at prior ingestion moments. This enables audit trails and reproducibility.

---

## How the Timelines Evolve Independently

A single relationship can undergo multiple changes that affect only one timeline:

1. **Valid-time extension**: A contract's end date is extended from 2022-06-01 to 2024-12-31
   - New `valid_until` value
   - New `recorded_at` timestamp for the amendment
   - Prior version gets `superseded_at` stamped

2. **Correction without valid-time change**: Fixing a typo in a relationship type
   - `valid_from`/`valid_until` unchanged
   - New `recorded_at` for the correction
   - Full provenance preserved

This separation ensures that domain semantics and system audit trails remain orthogonal concerns.

---

## Key Files and Their Roles

| File | Role |
|------|------|
| [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) | Core bi-temporal data model (`BiTemporalFact`, parsing/serialization) |
| [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) | Allen interval algebra operations on valid-time fields |
| [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) | Point-in-time and range queries across both timelines |
| [`semantica/provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/provenance/manager.py) | Transaction-time provenance tracking |
| [`docs/reference/temporal.md`](https://github.com/semantica-agi/semantica/blob/main/docs/reference/temporal.md) | Official documentation and API reference |

---

## Summary

- **Four timestamps per fact**: `valid_from`, `valid_until`, `recorded_at`, `superseded_at`
- **Independent axes**: Valid time tracks real-world truth; transaction time tracks system knowledge
- **Automatic defaults**: `recorded_at` auto-stamps at ingestion if omitted
- **Open-ended support**: `TemporalBound.OPEN` represents unbounded intervals
- **Query flexibility**: Reconstruct domain state or system state at any historical point
- **Full provenance**: Every amendment preserves prior versions with complete temporal metadata

---

## Frequently Asked Questions

### What happens if `recorded_at` is not provided when creating a relationship?

The `BiTemporalFact.from_relationship` factory falls back to the `valid_from` timestamp if present, otherwise uses `datetime.now(utc)` via the `_default_recorded_at` helper. This ensures every fact has a defined transaction time without requiring explicit user input.

### How does Semantica represent facts that are currently true or currently known?

Open-ended intervals use the `TemporalBound.OPEN` sentinel. For valid time, this means no known end date to the fact's truth. For transaction time, it means the fact has not yet been superseded by a newer version.

### Can I query for facts that were valid in the real world before the system recorded them?

Yes. Valid-time queries operate purely on `valid_from`/`valid_until`, independent of `recorded_at`. This allows retrospective modeling where domain history predates system ingestion—common when migrating legacy data into Semantica.

### What's the difference between `superseded_at` and `valid_until`?

`valid_until` marks when a fact ceased being true in reality (domain time). `superseded_at` marks when a newer version replaced it in the system (recorded time). A fact can be superseded with a correction while its valid time remains unchanged, or its valid time can extend without any system-level superseding event.