# Configuring Bi-Temporal Fact Tracking in Semantica: Valid-Time vs Recorded-At

> Master bi-temporal fact tracking in Semantica. Learn to configure valid-time versus recorded-at for precise temporal reasoning and audit trails. Enhance your AGI development.

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

---

**Semantica implements bi-temporal fact tracking through the `BiTemporalFact` wrapper class, which captures both valid-time (when a fact was true in reality) and recorded-time (when the system learned the fact) to enable precise temporal reasoning and audit trails.**

The Semantica framework stores graph relationships as plain dictionaries augmented with bi-temporal metadata, allowing developers to track when facts were true in the real world separately from when they entered the knowledge graph. This dual-axis approach, implemented in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py), powers time-travel queries, historical audits, and compliance reporting without duplicating data structures.

## Understanding Valid-Time vs Recorded-Time

Semantica's bi-temporal model maintains two independent time axes on every relationship edge.

**Valid-time** represents when a fact was true in the real world, typically expressed through `valid_from` and `valid_until` fields. This axis drives business logic, temporal reasoning, and point-in-time snapshots. You can use the `TemporalBound.OPEN` sentinel to indicate an ongoing interval with no defined end date.

**Recorded-time** (also called recorded-at) captures when the system learned about the fact, stored in the `recorded_at` and `superseded_at` fields. This axis drives provenance tracking, audit trails, and data lineage, showing when information entered the graph regardless of its real-world validity period.

Both axes coexist on the same edge, enabling queries against either timeline without data duplication.

## Core Architecture and Components

The bi-temporal implementation centers on the **`BiTemporalFact`** class defined in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py) (lines 27-41). This wrapper normalizes raw dictionary values into proper `datetime` objects and provides serialization methods for graph storage.

Key architectural components include:

- **`BiTemporalFact`** – Normalizes temporal fields and converts between relationship dictionaries and structured objects.
- **`TemporalGraphQuery`** ([`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py)) – Executes time-range queries against the knowledge graph.
- **`TemporalReasoning`** ([`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py)) – Applies reasoning operators that respect both temporal axes.
- **`ContextGraph`** ([`semantica/context.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/context.py)) – High-level API exposing `state_at()` for valid-time snapshots and `record_decision()` for explicit recorded-time overrides.

The system defaults `recorded_at` to `datetime.now(timezone.utc)` via the internal `_default_recorded_at()` function, but supports explicit backdating for data migration scenarios.

## Configuring Bi-Temporal Relationships

### Defining Valid-Time Intervals

To create a relationship with explicit valid-time bounds, pass `valid_from` and `valid_until` parameters when calling `graph.add_edge()`. For open-ended intervals, use the string `"OPEN"` or the `TemporalBound.OPEN` sentinel.

```python
from datetime import datetime, timezone
from semantica.context import ContextGraph

graph = ContextGraph(advanced_analytics=True)

# Valid-time: employment from 2022-01-01 to 2024-06-30

graph.add_edge(
    "alice_chen",
    "acme_corp",
    edge_type="works_for",
    valid_from="2022-01-01T00:00:00Z",
    valid_until="2024-06-30T00:00:00Z",
    recorded_at="2024-07-05T12:34:56Z",
)

```

For manual construction, use the `BiTemporalFact.from_relationship()` method (lines 44-58 of [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py)) to parse temporal fields, then convert back to a dictionary using `to_relationship_fields()`:

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

fact = BiTemporalFact.from_relationship({
    "valid_from": datetime(2025, 1, 1, tzinfo=timezone.utc),
    "valid_until": "OPEN",  # open-ended valid-time

    "recorded_at": datetime(2025, 1, 2, tzinfo=timezone.utc),
})

edge_dict = fact.to_relationship_fields()
graph.add_edge("bob_smith", "acme_corp", edge_type="works_for", **edge_dict)

```

### Managing Recorded-Time for Audit Trails

The `recorded_at` timestamp defaults to the current UTC time upon insertion, but you can override this for back-dating historical imports or replaying ingestion logs. According to the source code, passing `recorded_at` in the relationship dictionary overrides the default behavior of `_default_recorded_at()`.

This capability is essential for compliance pipelines that must reflect when documents were signed or when external systems generated data, distinct from when Semantica processed them.

### Querying Across Temporal Axes

For valid-time point-in-time snapshots, use **`ContextGraph.state_at()`**, which returns the graph as it existed at a specific world-time:

```python
snapshot_2023 = graph.state_at("2023-03-15")
print("Edges active on 2023-03-15:", snapshot_2023.get_edges())

```

For arbitrary interval queries across the valid-time axis, instantiate **`TemporalGraphQuery`** and call `query_time_range()`:

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

tq = TemporalGraphQuery()
facts = tq.query_time_range(
    kg=graph.to_kg_dict(),
    query="valid_facts",
    start_time="2023-01-01",
    end_time="2023-12-31",
)
print("Facts valid in 2023:", facts)

```

### Serializing for Compliance and Export

When exporting relationships for downstream audit systems, use `dumps_relationship_json()` to convert bi-temporal fields into JSON-compatible strings. The `serialize_temporal_bound()` function (lines 30-33 of [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py)) ensures open bounds are omitted (`None`) rather than exposing internal sentinel values.

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

rel = {
    "source": "alice_chen",
    "target": "acme_corp",
    "edge_type": "works_for",
    "valid_from": "2022-01-01T00:00:00Z",
    "valid_until": "2024-06-30T00:00:00Z",
    "recorded_at": "2024-07-05T12:34:56Z",
}
print(dumps_relationship_json(rel))

```

## Summary

- **Bi-temporal fact tracking** in Semantica uses the `BiTemporalFact` wrapper to maintain separate valid-time and recorded-time axes on every relationship.
- **Valid-time** (`valid_from`, `valid_until`) drives business logic and historical snapshots via `ContextGraph.state_at()`.
- **Recorded-time** (`recorded_at`, `superseded_at`) tracks system provenance and defaults to `datetime.now(timezone.utc)` unless explicitly overridden.
- The **`TemporalBound.OPEN`** sentinel represents ongoing intervals without end dates, serialized as `None` for JSON compatibility.
- Query capabilities include point-in-time snapshots (`state_at()`) and range queries (`TemporalGraphQuery.query_time_range()`).
- All temporal utilities reside in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py), [`temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_query.py), and [`temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_reasoning.py).

## Frequently Asked Questions

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

Valid-time indicates when a fact was true in the real world using `valid_from` and `valid_until` fields, while recorded-time indicates when the system learned about the fact via the `recorded_at` timestamp. Valid-time drives temporal reasoning and business logic, whereas recorded-time supports audit trails and data provenance tracking.

### How do I represent an ongoing fact with no end date in Semantica?

Use the **`TemporalBound.OPEN`** sentinel (or the string `"OPEN"`) in the `valid_until` field to indicate an open-ended interval. When serialized via `serialize_temporal_bound()` in [`temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/temporal_model.py), these open bounds convert to `None` for JSON compatibility while maintaining internal consistency for temporal queries.

### Can I backdate the recorded_at timestamp when importing historical data?

Yes, explicitly pass the `recorded_at` parameter when calling `graph.add_edge()` or include it in the relationship dictionary processed by `BiTemporalFact.from_relationship()`. This overrides the default `_default_recorded_at()` behavior and allows you to preserve the original ingestion time from legacy systems or document signatures.

### Which method should I use for point-in-time graph snapshots?

Use **`ContextGraph.state_at()`** to retrieve a snapshot of the entire graph as it existed at a specific valid-time instant. For querying specific fact intervals rather than full graph states, use `TemporalGraphQuery.query_time_range()` with the appropriate start and end time parameters.