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

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. 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
@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.

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. 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. 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?"

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?"

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 to implement Allen's interval algebra.

Managing Versioned Snapshots

For auditability and long-term storage, TemporalVersionManager (also in semantica/kg/temporal_query.py around line 1410) creates immutable, labeled snapshots.

Creating and Persisting Versions

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

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:

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 BiTemporalFact, parsers, bounds
Query engine semantica/kg/temporal_query.py TemporalGraphQuery, TemporalVersionManager
Interval algebra semantica/kg/temporal_reasoning.py TemporalReasoningEngine, TemporalInterval
Storage backends semantica/change_management/managers.py InMemoryVersionStorage, SQLiteVersionStorage

Summary

  • BiTemporalFact in temporal_model.py normalizes four timestamp fields for dual-axis tracking.
  • TemporalGraphQuery in 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →