# How to Configure Bi-Temporal Facts Using valid_time vs recorded_time in Semantica

> Learn to configure bi-temporal facts in Semantica's knowledge graph using valid_time vs recorded_time. Understand how Semantica manages system and business timestamps for accurate historical data.

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

---

**Semantica manages bi-temporal data through two orthogonal dimensions—`valid_time` representing when a fact is true in the real world, and `recorded_at` tracking when the system ingested the fact—with the `ProvenanceManager` automatically handling system timestamps while allowing explicit control over business-time validity periods.**

The `semantica-agi/semantica` repository implements a bi-temporal knowledge graph that distinguishes between when facts are true in reality versus when they were recorded in the system. Understanding how to configure bi-temporal facts using `valid_time` versus `recorded_time` enables sophisticated audit trails and historical reasoning across your data.

## Understanding Valid-Time vs Recorded-Time

Semantica stores temporal information across two independent axes. This separation allows you to answer both "When was this true?" and "When did we know it?" simultaneously.

### Valid-Time (Business Time)

**Valid-time** represents the period during which a fact is intended to be true in the real world. In Semantica's implementation, this is typically expressed as `valid_from` and `valid_until` timestamps on entities, or as a single `valid_time` property on relationships. You populate these values from source data or supply them explicitly when creating facts.

### Recorded-Time (System Time)

**Recorded-time** (stored as `recorded_at`) indicates when the system actually recorded the fact. This dimension serves audit, versioning, and provenance requirements. According to the source code in [`semantica/provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/provenance/manager.py), the `ProvenanceManager` automatically injects the current ISO-8601 timestamp via `datetime.now().isoformat()` if you omit this field when adding entries (see lines 907-913 in the `_record_entry()` method).

## Adding Bi-Temporal Facts to the Graph

When constructing your knowledge graph, you explicitly set valid-time boundaries while allowing the system to handle recorded-time automatically.

### Setting Valid-Time Properties

Use the `GraphBuilder` to create relationships with explicit `valid_time` properties. The system reads this property and stores it as `valid_from` on the edge, as implemented in `GraphBuilder.load_from_neo4j()` at lines 1204-1208 of [`semantica/kg/graph_builder.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/graph_builder.py).

```python
from semantica.kg.graph_builder import GraphBuilder

builder = GraphBuilder()
builder.add_relationship(
    source_id="employee_001",
    target_id="company_acme",
    rel_type="EMPLOYED_AT",
    properties={
        "valid_time": "2023-01-01T00:00:00Z/2025-12-31T23:59:59Z",  # Business time

        "department": "Engineering"
        # "recorded_at" will be added automatically by ProvenanceManager

    }
)

```

### Automatic Recorded-Time Assignment

If you do not provide `recorded_at` in your payload, the `ProvenanceManager` automatically assigns the current system time. You only need to supply this field explicitly when you require a custom audit timestamp, such as when backfilling historical data with specific ingestion dates. The test suite in [`tests/test_401_temporal_provenance_export.py`](https://github.com/semantica-agi/semantica/blob/main/tests/test_401_temporal_provenance_export.py) (lines 40-58) validates this auto-population behavior.

## Querying Bi-Temporal Data

The `ProvenanceManager` exposes specific query helpers that filter on either temporal dimension independently or in combination.

### Query by Valid-Time with query_as_of()

To retrieve facts that were valid at a specific business date regardless of when they were recorded, use the `query_as_of()` method. This method is defined at lines 1498-1510 in [`semantica/provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/provenance/manager.py).

```python
from semantica.provenance.manager import ProvenanceManager

prov_mgr = ProvenanceManager()

# Get all facts valid on March 15, 2024

current_facts = prov_mgr.query_as_of("2024-03-15T00:00:00Z")

```

### Query by Recorded-Time with query_recorded_between()

For audit purposes, filter facts by when they entered the system using `query_recorded_between()`. This returns only facts recorded within your specified audit window.

```python

# Retrieve facts recorded in Q1 2024 for compliance auditing

recent_facts = prov_mgr.query_recorded_between(
    "2024-01-01T00:00:00Z",
    "2024-03-31T23:59:59Z"
)

```

### Combining Both Dimensions

True bi-temporal queries answer what was known as of a certain valid-time and what had been recorded by a specific audit time. Combine both filtering methods to achieve this.

```python

# Facts valid on June 1, 2023 that were recorded by January 1, 2024

bi_temporal = prov_mgr.query_as_of("2023-06-01T00:00:00Z")
bi_temporal = [
    f for f in bi_temporal
    if f["recorded_at"] <= "2024-01-01T00:00:00Z"
]

```

The comprehensive test suite in [`tests/test_395_temporal_semantics_comprehensive.py`](https://github.com/semantica-agi/semantica/blob/main/tests/test_395_temporal_semantics_comprehensive.py) (lines 240-254) validates that facts with `valid_from` values are queried correctly while `recorded_at` handles audit filtering independently.

## Implementation Details in the Source Code

Several key components handle the bi-temporal logic:

- **[`semantica/kg/graph_builder.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/graph_builder.py)** (`load_from_neo4j()`, lines 1204-1208): Extracts the `valid_time` relationship property and persists it as `valid_from` on graph edges.

- **[`semantica/provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/provenance/manager.py)** (`ProvenanceManager` class): Contains `_record_entry()` (lines 907-913) for automatic `recorded_at` injection, and the query primitives `query_as_of()` and `query_recorded_between()` (lines 1498-1510).

- **[`tests/test_401_temporal_provenance_export.py`](https://github.com/semantica-agi/semantica/blob/main/tests/test_401_temporal_provenance_export.py)** (lines 40-58): Confirms that records receive automatic `recorded_at` timestamps when none are provided.

- **[`tests/test_395_temporal_semantics_comprehensive.py`](https://github.com/semantica-agi/semantica/blob/main/tests/test_395_temporal_semantics_comprehensive.py)** (lines 240-254): Validates the interaction between valid-time boundaries and recorded-time audit filters.

## Summary

- **Bi-temporal facts** separate business time (`valid_time`) from system time (`recorded_at`) to enable historical reasoning and audit trails.

- **Provide `valid_time`** on relationships or `valid_from`/`valid_until` on entities to define when facts are true in reality.

- **Allow auto-population** of `recorded_at` by the `ProvenanceManager` unless you need custom audit timestamps for backfilled data.

- **Query by valid-time** using `query_as_of()` to see the business perspective at any historical point.

- **Query by recorded-time** using `query_recorded_between()` for system audit and compliance checks.

- **Combine both filters** to answer sophisticated bi-temporal questions about what was known when.

## Frequently Asked Questions

### What is the difference between valid_time and recorded_at in Semantica?

`valid_time` represents the business period when a fact is true in the real world, while `recorded_at` tracks when the system actually stored the fact. The former answers "When did this happen?" and the latter answers "When did we learn about it?" These orthogonal dimensions enable bi-temporal reasoning across your knowledge graph.

### How does Semantica automatically handle recorded_at timestamps?

The `ProvenanceManager` automatically injects `recorded_at` using `datetime.now().isoformat()` when you add facts without specifying this field. This occurs in the `_record_entry()` method within [`semantica/provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/provenance/manager.py) (lines 907-913), ensuring every fact has a system audit timestamp even if you only supply valid-time boundaries.

### Can I query for facts that were valid at a certain time but recorded later?

Yes. First call `query_as_of()` with your target valid-time, then filter the results by checking the `recorded_at` field against your audit cutoff date. This bi-temporal pattern allows you to reconstruct what information was available to the system as of a specific audit date while respecting the actual business validity periods of the underlying facts.

### Where are bi-temporal properties stored in the Semantica codebase?

Valid-time properties reside in the knowledge graph layer ([`semantica/kg/graph_builder.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/kg/graph_builder.py)), where `valid_time` on relationships becomes `valid_from` on edges. Recorded-time properties are managed by the provenance layer ([`semantica/provenance/manager.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/provenance/manager.py)), which handles automatic timestamp assignment and provides the query APIs for both temporal dimensions.