What Is a Bi-Temporal Fact in Semantica? Temporal Knowledge Graph Modeling Explained

A bi-temporal fact is a backward-compatible wrapper that adds valid-time and transaction-time dimensions to knowledge graph relationships, enabling point-in-time queries and versioning without breaking legacy code.

In the semantica-agi/semantica repository, every relationship in the knowledge graph can be enriched with temporal metadata through the bi-temporal fact abstraction. This pattern captures when a fact is true in the real world (valid time) and when it was recorded in the system (transaction time), providing a robust foundation for temporal reasoning and historical analysis.

What Is a Bi-Temporal Fact?

According to the Semantica source code in semantica/kg/temporal_model.py, a bi-temporal fact is a specialized wrapper around a plain relationship dictionary. The class BiTemporalFact (lines 27-41) injects four temporal fields into every edge, allowing the knowledge graph to track when a statement is true independently from when it was stored.

The implementation maintains backward compatibility—the wrapper can be created from an existing relationship dict via from_relationship() and can flatten back into a dict via to_relationship_fields(), ensuring legacy code that reads or writes valid_from or valid_until continues to function unchanged.

Core Temporal Fields

A bi-temporal fact tracks two distinct timelines through four fields:

  • valid_from (datetime | None): The valid-time when the fact becomes true in the modeled world.
  • valid_until (datetime | TemporalBound | None): The valid-time when the fact stops being true. Uses the sentinel TemporalBound.OPEN for open-ended validity.
  • recorded_at (datetime): The transaction-time when the fact was written to the graph. Auto-populated with current UTC time if omitted.
  • superseded_at (datetime | TemporalBound): The transaction-time when the fact was logically removed or updated. Also uses TemporalBound.OPEN for active facts.

This dual-axis model (valid vs. transaction time) enables point-in-time snapshots and audit trails without mutating historical records.

How Semantica Uses Bi-Temporal Facts

1. Normalization of Incoming Relationships

When a relationship is ingested from extraction pipelines, the function deserialize_relationship_temporal_fields calls BiTemporalFact.from_relationship() (lines 36-44 in temporal_model.py). This guarantees that all four temporal fields are present and correctly typed before the edge enters the knowledge graph.

2. Temporal Query Engine

The query engine retrieves valid and transaction intervals via helpers defined in semantica/kg/temporal_query.py (lines 48-55). These utilities build a BiTemporalFact instance and return the requested axis—either valid or transaction—enabling time-slice queries like "What was true on March 15, 2023?"

3. Temporal Reasoning

Reasoning algorithms in semantica/kg/temporal_reasoning.py (lines 46-50) coerce raw facts into BiTemporalFact objects before extracting validity intervals. This ensures that causal ordering and pattern detection operate on consistently typed temporal bounds.

4. Versioning and Provenance

The TemporalVersionManager (referenced in the temporal model implementation) creates new BiTemporalFact instances with fresh recorded_at timestamps to store graph snapshots. This supports historical provenance tracking and rollback capabilities.

5. Serialization

When exporting relationships to JSON or RDF, the helper relationship_to_json_ready (lines 47-55 in temporal_model.py) serializes temporal fields using ISO-8601 format, ensuring consistent representation across external systems.

Working with Bi-Temporal Facts: Code Examples

Creating a Bi-Temporal Fact from Raw Data

from semantica.kg import BiTemporalFact

raw = {
    "subject": "Alice",
    "predicate": "employed_at",
    "object": "AcmeCorp",
    "valid_from": "2022-01-01",
    "valid_until": None,  # open-ended valid time

    # transaction-time fields are optional

}

fact = BiTemporalFact.from_relationship(raw)

print(fact.valid_from)      # 2022-01-01T00:00:00+00:00 (datetime)

print(fact.valid_until)     # TemporalBound.OPEN

print(fact.recorded_at)     # auto-populated current UTC datetime

print(fact.superseded_at)   # TemporalBound.OPEN

Converting Back to Dictionary Format


# Flatten for storage or export

relationship_ready = fact.to_relationship_fields()

# Result contains ISO-8601 strings:

# {

#   "valid_from": "2022-01-01T00:00:00Z",

#   "valid_until": None,

#   "recorded_at": "2024-09-10T12:34:56Z",

#   "superseded_at": None,

# }

Using in Temporal Queries

from datetime import datetime, timezone
from semantica.kg import TemporalVersionManager

# Assume kg is a loaded knowledge graph

tvm = TemporalVersionManager(kg)

# Find relationships valid on a specific date

target_date = datetime(2023, 3, 15, tzinfo=timezone.utc)

snapshot = tvm.snapshot_at(timestamp="2023-03-15")
valid_edges = [
    e for e in snapshot["relationships"]
    if BiTemporalFact.from_relationship(e).valid_from <= target_date <
       (BiTemporalFact.from_relationship(e).valid_until or datetime.max.replace(tzinfo=timezone.utc))
]

Key Implementation Files

File Role
semantica/kg/temporal_model.py Defines BiTemporalFact, parsers, and serializers (lines 27-55).
semantica/kg/temporal_query.py Query engine helpers extracting valid/transaction bounds.
semantica/kg/temporal_reasoning.py Reasoning algorithms coercing facts into bi-temporal objects.
semantica/kg/__init__.py Re-exports BiTemporalFact for the public API.
tests/test_395_temporal_semantics_comprehensive.py Unit tests verifying construction and conversion.
tests/semantic_extract/test_temporal_extraction.py Integration tests for extraction → normalization pipeline.

Summary

  • A bi-temporal fact is the canonical representation of a time-stamped edge in Semantica, unifying valid-time and transaction-time axes.
  • The BiTemporalFact class in semantica/kg/temporal_model.py provides backward-compatible wrapping via from_relationship() and to_relationship_fields().
  • Four temporal fields (valid_from, valid_until, recorded_at, superseded_at) enable point-in-time queries and audit trails.
  • All higher-level temporal features—querying, reasoning, versioning—depend on this unified model.

Frequently Asked Questions

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

Valid time refers to when a fact is true in the real world (captured by valid_from and valid_until), while transaction time records when the fact was stored in the knowledge graph (captured by recorded_at and superseded_at). This distinction allows Semantica to answer both "What was true last year?" and "When did we learn about it?"

How does BiTemporalFact maintain backward compatibility with legacy relationship dictionaries?

The class provides bidirectional conversion methods: from_relationship() ingests plain dictionaries and populates missing temporal fields with defaults, while to_relationship_fields() exports the temporal data back to dictionary format. Legacy code reading valid_from or valid_until continues to work because these fields remain standard dictionary keys in the output.

What is TemporalBound.OPEN and when is it used?

TemporalBound.OPEN is a sentinel value indicating that a time interval has no defined end. It is used for valid_until when a fact is currently true, and for superseded_at when a fact remains active in the latest transaction time. This avoids null ambiguity in temporal comparisons.

Which Semantica modules handle bi-temporal fact serialization?

Serialization logic resides primarily in semantica/kg/temporal_model.py, specifically in relationship_to_json_ready and related helpers (lines 47-55). The TemporalVersionManager utilizes these serializers when creating version snapshots, ensuring ISO-8601 compliant output for downstream consumers.

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 →