How Semantica Handles Bi-Temporal Facts: Architecture and Implementation Guide

Semantica stores every relationship as a bi-temporal fact that tracks both valid time (when a fact is true in the real world) and transaction time (when it was recorded in the system), using the BiTemporalFact dataclass in semantica/kg/temporal_model.py to normalize timestamps and handle open-ended intervals.

The open-source Semantica framework (semantica-agi/semantica) implements a robust bi-temporal data model that enables powerful time-travel queries and audit capabilities across its knowledge graph. Unlike standard graph databases that store only current state, Semantica treats time as a first-class citizen by persisting four distinct timestamps for every relationship.

Understanding Valid Time vs. Transaction Time

Semantica’s bi-temporal facts maintain two independent orthogonal timelines, ensuring complete historical accuracy and auditability:

  • Valid time represents when a fact is true in reality, controlled by the valid_from and valid_until fields. This allows the system to answer questions like "Who was the CEO on March 15, 2020?" regardless of when that information was entered into the database.
  • Transaction time records the system’s history of changes via recorded_at (when the fact was first recorded) and superseded_at (when it was updated or replaced). This creates an immutable audit trail showing exactly when data corrections occurred.

According to the Semantica source code, the core implementation lives entirely within semantica/kg/temporal_model.py, where the BiTemporalFact dataclass provides a thin wrapper around raw relationship dictionaries, centralizing all parsing, validation, and serialization logic to ensure consistency across the codebase.

Core Implementation in temporal_model.py

The BiTemporalFact Dataclass

The foundation of bi-temporal handling is the BiTemporalFact dataclass, which exposes the four critical timestamp fields while normalizing heterogeneous input formats. When constructing facts, the system accepts ISO strings, epoch numbers, or native datetime objects, converting them internally to timezone-aware UTC timestamps.

The dataclass is designed for backward compatibility: existing code can continue reading and writing raw dictionary fields (valid_from, valid_until, recorded_at, superseded_at), while the wrapper guarantees consistent validation and parsing semantics.

Handling Open-Ended Intervals with TemporalBound

Semantica uses a dedicated TemporalBound enum to represent unbounded time intervals. This enum contains a single sentinel value, OPEN, which the reasoning engine interprets as datetime.max for valid_until and datetime.min for superseded_at.

Using TemporalBound.OPEN rather than null values allows the query engine to perform deterministic range comparisons without special-case null handling, simplifying the implementation of temporal logic operators.

Factory Methods and Serialization

The BiTemporalFact.from_relationship(rel) factory method constructs instances from plain relationship dictionaries, automatically filling defaults for missing transaction-time fields and assigning TemporalBound.OPEN to unspecified end dates. This method handles all timestamp normalization and validation during ingestion.

For persistence, the to_relationship_fields() method converts the internal dataclass back into a JSON-serializable dictionary, ensuring that all datetime objects render as ISO-8601 strings while mapping TemporalBound.OPEN back to None for compatibility with standard JSON stores.

Creating and Serializing Bi-Temporal Facts

You can construct bi-temporal facts directly from relationship data using the factory method, which infers transaction timestamps when omitted:

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",
}

fact = BiTemporalFact.from_relationship(rel)

print(fact.valid_from)      # datetime(2018, 1, 1, tzinfo=UTC)

print(fact.valid_until)     # datetime(2022, 6, 1, tzinfo=UTC)

print(fact.recorded_at)     # Current UTC timestamp (auto-filled)

print(fact.superseded_at)   # TemporalBound.OPEN

To serialize the fact for storage or API response, convert it back to a plain dictionary with normalized string formats:

fields = fact.to_relationship_fields()
print(fields["valid_from"])      # "2018-01-01T00:00:00Z"

print(fields["valid_until"])     # "2022-06-01T00:00:00Z"

print(fields["recorded_at"])     # "2024-09-08T12:34:56Z"

print(fields["superseded_at"])   # None (represents OPEN)

Querying Bi-Temporal Data

Point-in-Time Reconstruction with TemporalGraphQuery

The TemporalGraphQuery class leverages bi-temporal metadata to reconstruct knowledge graph snapshots as they existed at arbitrary moments in real-world history. This module filters relationships based on valid-time overlap with the query timestamp, returning only facts that were actually true during the specified period.

from semantica.kg import GraphBuilder, TemporalGraphQuery

builder = GraphBuilder()
kg = builder.build(sources=[{"entities": [], "relationships": [rel]}])

query = TemporalGraphQuery(temporal_granularity="day")
snapshot = query.reconstruct_at_time(kg, "2020-03-15")
print(snapshot["relationships"])   # Only relationships valid on 2020-03-15

Version Management with TemporalVersionManager

For audit and compliance requirements, TemporalVersionManager handles transaction-time versioning, creating immutable snapshots that preserve the complete history of when facts were recorded or superseded. This enables "as-of" queries against the transaction timeline, distinct from valid-time queries.

from semantica.kg import TemporalVersionManager

versioner = TemporalVersionManager()
versioner.create_snapshot(
    kg,
    version_label="2024-Q1",
    author="alice@example.com",
    description="Quarter-1 snapshot after reorganisations",
)

for v in versioner.list_versions():
    print(v["label"], v["timestamp"])

Integration with the Knowledge Graph Stack

The bi-temporal model integrates across multiple specialized modules:

  • semantica/kg/temporal_query.py: Implements TemporalGraphQuery for range queries, pattern detection, and temporal filtering using valid-time intervals.
  • semantica/kg/temporal_reasoning.py: Provides Allen interval algebra operations over TemporalInterval objects derived from BiTemporalFact instances, enabling sophisticated temporal logic such as "before," "during," and "overlaps" relationships.
  • semantica/kg/temporal_version_manager.py: Manages persistent versioned snapshots and supersession semantics using the transaction-time fields.

For complete API documentation, see docs/reference/temporal.md in the repository, which describes the bi-temporal query language and versioning strategies.

Summary

  • Dual timeline tracking: Semantica implements bi-temporal facts using four timestamp fields (valid_from, valid_until, recorded_at, superseded_at) to distinguish between real-world validity and system record history.
  • Centralized validation: The BiTemporalFact dataclass in semantica/kg/temporal_model.py normalizes all timestamp formats and enforces consistent open-ended interval semantics via TemporalBound.OPEN.
  • Flexible querying: TemporalGraphQuery reconstructs point-in-time valid-time snapshots, while TemporalVersionManager preserves transaction-time audit trails.
  • Composable architecture: Bi-temporal metadata flows through the query engine, reasoning module (Allen algebra), and versioning system to enable complex temporal intelligence without breaking existing graph operations.

Frequently Asked Questions

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

Valid time indicates when a fact was actually true in the real world (e.g., "Alice was CEO from 2018 to 2022"), while transaction time tracks when the system recorded or modified that fact (e.g., "We learned about Alice's tenure on January 15, 2018 and updated the record on March 3, 2022"). This separation allows Semantica to answer both "What did we know then?" and "What was true then?" questions independently.

How does Semantica handle relationships with no known end date?

Semantica uses the TemporalBound.OPEN sentinel value defined in semantica/kg/temporal_model.py to represent unbounded valid-time or transaction-time intervals. When valid_until or superseded_at is set to TemporalBound.OPEN, the reasoning engine treats these as datetime.max or datetime.min respectively, ensuring that range comparisons succeed without special null handling.

Can I query the knowledge graph as it appeared on a specific date in the past?

Yes. The TemporalGraphQuery class provides the reconstruct_at_time() method, which takes a knowledge graph and a target date string, then returns a snapshot containing only relationships whose valid-time intervals overlap with that date. This enables point-in-time analysis of historical graph states without maintaining separate database snapshots.

Is the bi-temporal model backward compatible with standard graph operations?

Yes. According to the Semantica source code, the BiTemporalFact wrapper is designed for backward compatibility: existing code can continue reading and writing raw relationship dictionaries containing valid_from, valid_until, and other temporal fields. The wrapper only enforces validation and normalization when explicitly invoked via from_relationship() or to_relationship_fields().

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 →