# How Semantica Handles Bi-Temporal Facts in the ContextGraph: A Complete Guide

> Semantica manages bi-temporal facts in ContextGraph with four timestamps for validity and history. Achieve point-in-time snapshots, audit trails, and temporal reasoning.

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

---

**Semantica stores every relationship as a bi-temporal fact using four timestamps—`valid_from`/`valid_until` for real-world validity and `recorded_at`/`superseded_at` for system history—enabling point-in-time snapshots, full audit trails, and Allen algebra-based temporal reasoning.**

The **semantica-agi/semantica** repository implements a sophisticated bi-temporal data model within its ContextGraph, ensuring that every edge captures both *when* a fact was true in reality and *when* it was recorded in the system. This dual-timeline approach prevents data loss during updates and supports complex historical queries across the knowledge graph.

## The BiTemporalFact Data Model

At the core of Semantica’s temporal support is the `BiTemporalFact` dataclass defined in [`semantica/kg/temporal_model.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_model.py). This class wraps every relationship with explicit temporal boundaries, distinguishing between two independent but equally critical timelines.

### Valid-Time vs. Transaction-Time

Each bi-temporal fact tracks four distinct timestamp fields:

- **Valid-time (`valid_from`, `valid_until`)**: Represents when the relationship held true in the real world. This interval is independent of the database state and answers "when was this fact actually valid?"
- **Transaction-time (`recorded_at`, `superseded_at`)**: Represents when the fact was stored or replaced in the ContextGraph. This provides the audit trail and answers "when did we know this?"

The system uses `TemporalBound.OPEN` to model intervals that extend indefinitely, allowing facts to remain valid or current without arbitrary end dates.

### Factory Pattern and Normalization

The `BiTemporalFact` class provides the `from_relationship()` factory method to convert raw relationship dictionaries into fully-typed temporal objects. This method handles missing values, applies UTC normalization, and sets appropriate defaults for open-ended intervals. As implemented in `semantica-agi/semantica`, the factory ensures that legacy data without temporal fields gracefully degrades to sensible defaults while preserving type safety.

## Building the ContextGraph with Temporal Support

When constructing the graph, `GraphBuilder` (located in [`semantica/kg/graph_builder.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/graph_builder.py)) integrates bi-temporal facts through the `deserialize_relationship_temporal_fields()` utility. Initializing the builder with `enable_temporal=True` activates strict validation and automatic timestamp normalization for every incoming relationship.

During the build process, the ContextGraph normalizes each edge’s temporal fields before storage. This guarantees that downstream components can rely on consistent datetime representations and non-null temporal boundaries, regardless of input format variations.

## Querying and Reasoning Over Time

The bi-temporal foundation enables three primary capabilities that distinguish Semantica from static knowledge graphs.

### Point-in-Time Snapshots

The `TemporalGraphQuery` class in [`semantica/kg/temporal_query.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_query.py) provides `query_at_time()` for reconstructing historical states. This method filters the ContextGraph to return only entities and relationships whose **valid-time** interval contains the requested timestamp.

When you query the graph "as of" June 15, 2023, the engine excludes relationships that started after that date or ended before it, producing a self-consistent sub-graph that reflects the real-world state at that exact moment.

### Transaction-Time Versioning and Auditing

The `TemporalVersionManager` leverages `recorded_at` and `superseded_at` to maintain immutable history. When `apply_revision()` processes an update, it marks the existing fact with a `superseded_at` timestamp and inserts the new fact with a fresh `recorded_at` value.

This mechanism creates a complete audit trail without destructive updates, enabling "time-travel" queries that ask "what did the graph look like when we recorded this change?" rather than just "what was true in reality?"

### Temporal Reasoning with Allen Algebra

The `TemporalReasoningEngine` in [`semantica/kg/temporal_reasoning.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/temporal_reasoning.py) applies formal **Allen interval algebra** to `BiTemporalFact` objects. It computes temporal relationships such as overlap, containment, precedence, and gaps between intervals across both the valid-time and transaction-time dimensions.

This engine powers advanced analytics including temporal path finding, conflict detection between overlapping claims, and consistency checking across the knowledge graph.

## Practical Implementation Examples

The following patterns demonstrate how to create, store, query, and revise bi-temporal facts in the ContextGraph.

```python

# Create a bi-temporal fact from raw relationship data

from semantica.kg import BiTemporalFact

raw_rel = {
    "subject": "Customer123",
    "predicate": "has_subscription",
    "object": "PremiumPlan",
    "valid_from": "2023-01-01",
    "valid_until": "2024-01-01",
    # recorded_at auto-filled; superseded_at defaults to OPEN

}
fact = BiTemporalFact.from_relationship(raw_rel)

```

```python

# Build the ContextGraph with temporal normalization enabled

from semantica.kg import GraphBuilder

builder = GraphBuilder(enable_temporal=True)
graph = builder.build(sources=[{"relationships": [raw_rel]}])

```

```python

# Query a point-in-time snapshot using valid-time filtering

from semantica.kg import TemporalGraphQuery

tgq = TemporalGraphQuery()
snapshot = tgq.query_at_time(graph, query="", at_time="2023-06-15")

# Returns only relationships valid on June 15, 2023

```

```python

# Supersede the previous fact with a new revision

new_rel = {
    "subject": "Customer123",
    "predicate": "has_subscription",
    "object": "BasicPlan",
    "valid_from": "2024-01-02",
    "valid_until": "2025-01-01",
    "recorded_at": "2024-01-02T12:00:00Z",  # Transaction-time of this update

}
new_fact = BiTemporalFact.from_relationship(new_rel)
graph["relationships"].append(new_fact.to_relationship_fields())

```

## Summary

- **Bi-temporal facts** in Semantica capture both real-world validity (`valid_from`/`valid_until`) and system history (`recorded_at`/`superseded_at`) within the `BiTemporalFact` dataclass.
- **`GraphBuilder`** normalizes temporal fields during ingestion, ensuring consistent storage in [`semantica/kg/graph_builder.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/graph_builder.py).
- **Point-in-time queries** reconstruct historical graph states by filtering on valid-time intervals using `TemporalGraphQuery`.
- **Immutable versioning** preserves full audit trails through transaction-time timestamps managed by `TemporalVersionManager`.
- **Temporal reasoning** applies Allen interval algebra via `TemporalReasoningEngine` to detect conflicts, overlaps, and temporal paths.

## Frequently Asked Questions

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

**Valid-time** indicates when a fact was true in the real world, using `valid_from` and `valid_until` fields, while **transaction-time** tracks when the fact was recorded or superseded in the ContextGraph using `recorded_at` and `superseded_at`. This separation allows the system to distinguish between historical reality and the history of database changes.

### How does Semantica handle relationships that are currently valid?

Semantica uses `TemporalBound.OPEN` to represent unbounded intervals. When a relationship has no specified end date, `valid_until` defaults to OPEN, indicating the fact remains valid indefinitely. Similarly, `superseded_at` remains OPEN until a newer version explicitly closes the interval.

### Can I query the ContextGraph as it existed on a specific date?

Yes. The `TemporalGraphQuery.query_at_time()` method filters relationships to include only those whose valid-time range contains the specified timestamp. This returns a consistent sub-graph representing the real-world state at that exact moment, excluding future or expired relationships.

### What temporal operations does the TemporalReasoningEngine support?

The `TemporalReasoningEngine` implements **Allen interval algebra** to compute relationships between temporal intervals, including overlaps, meets, contains, starts, finishes, and precedes. These operations apply across both valid-time and transaction-time dimensions to enable conflict detection, consistency validation, and temporal path analysis.