# How to Implement Time-Travel Queries Using ContextGraph.state_at() in Semantica

> Learn to implement time-travel queries in Semantica using ContextGraph.state_at(). Retrieve historical graph snapshots with ISO-8601 strings, datetime objects, or Unix timestamps.

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

---

**Use `ContextGraph.state_at()` to retrieve an immutable, serializable snapshot of the graph at any historical moment by passing an ISO-8601 string, `datetime` object, or Unix epoch timestamp.**

The semantica-agi/semantica framework provides native temporal support through the `ContextGraph` class, enabling you to audit historical states and reconstruct decision trails without mutating live data. By leveraging the `state_at()` method, you can implement robust **time-travel queries** that filter entities and relationships by their validity windows, making it ideal for compliance tracking and temporal reasoning applications.

## How ContextGraph.state_at() Works Internally

The `state_at()` method in [`semantica/context/context_graph.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/context/context_graph.py) (lines 2499-2529) constructs historical views through three deterministic stages that validate temporal boundaries against your query timestamp.

### Timestamp Normalization

Before filtering, the method normalizes your input via `ContextGraph._normalize_timestamp()` (lines 2510-2515). This internal helper accepts **ISO-8601 strings**, **Unix epoch integers**, or **timezone-aware `datetime` objects**, converting any format into a timezone-naive UTC `datetime` instance. This ensures consistent comparison logic regardless of how you specify the query moment.

### Temporal Validity Checks

Each graph component implements an `is_active(at_time)` method that evaluates validity windows:

- **`ContextNode.is_active()`** (lines 333-350): Returns `True` only when `valid_from` ≤ `at_time` ≤ `valid_until`.
- **`ContextEdge.is_active()`** (lines 996-1013): Applies identical boundary checks to relationships.

These methods enforce **closed-interval semantics**, meaning entities are visible at their exact start and end timestamps.

### Snapshot Construction

The final phase assembles a dictionary containing:

- `timestamp`: The normalized UTC query time.
- `nodes`/`entities`: Active node payloads.
- `edges`/`relationships`: Active edge payloads.
- `decisions`: Temporal views of decision-type nodes (lines 2515-2528).

Because this operation creates a **read-only view**, you can safely execute multiple time-travel queries to reconstruct history or compare divergent states without side effects.

## Practical Implementation Examples

### Querying with Different Timestamp Formats

The following example demonstrates adding temporally bounded nodes and edges, then retrieving snapshots using three supported input types:

```python
from semantica.context import ContextGraph
from datetime import datetime, timezone

g = ContextGraph()

# Create entities with explicit validity windows

g.add_node(
    "patient_123",
    node_type="entity",
    content="John Doe",
    valid_from="2023-01-01T00:00:00Z",
    valid_until="2023-12-31T23:59:59Z",
)

g.add_edge(
    "patient_123",
    "diagnosis_456",
    edge_type="has_diagnosis",
    valid_from="2023-05-01",
    valid_until="2023-07-31",
)

# Three equivalent ways to query June 15, 2023

snapshot_iso = g.state_at("2023-06-15T12:00:00Z")
snapshot_dt  = g.state_at(datetime(2023, 6, 15, tzinfo=timezone.utc))
snapshot_ts  = g.state_at(1686825600)  # Unix epoch seconds

assert "patient_123" in [n["id"] for n in snapshot_iso["nodes"]]
assert any(e["type"] == "has_diagnosis" for e in snapshot_iso["edges"])

```

### Reconstructing Historical Views After Retractions

When you retract an entity using `retract_node()`, the operation updates the `valid_until` boundary rather than deleting data. This preserves historical views while hiding entities from future snapshots:

```python

# Close the patient's validity as of August 1, 2023

g.retract_node(kind="entity", node_id="patient_123", at="2023-08-01T00:00:00Z")

# Future queries exclude the retracted node

future = g.state_at("2023-09-01")
assert not any(n["id"] == "patient_123" for n in future["nodes"])

# Past queries still include the entity

past = g.state_at("2023-07-01")
assert any(n["id"] == "patient_123" for n in past["nodes"])

```

### Auditing Decisions at Specific Points in Time

The `decisions` key provides filtered access to historical decision records. This pattern supports compliance audits and provenance tracking:

```python
g.record_decision(
    category="loan_approval",
    scenario="first_homebuyer",
    reasoning="good credit",
    outcome="approved",
    confidence=0.95,
    entities=["customer_001"],
    valid_from="2024-01-01",
    valid_until="2024-06-30",
)

g.record_decision(
    category="loan_approval",
    scenario="first_homebuyer",
    reasoning="low income",
    outcome="rejected",
    confidence=0.85,
    entities=["customer_001"],
    valid_from="2024-07-01",
    valid_until="2024-12-31",
)

# February snapshot returns the approval

snap_feb = g.state_at("2024-02-15")
print([d["outcome"] for d in snap_feb["decisions"]])  # ['approved']

# August snapshot returns the rejection

snap_aug = g.state_at("2024-08-01")
print([d["outcome"] for d in snap_aug["decisions"]])  # ['rejected']

```

## Summary

- **`ContextGraph.state_at()`** is the primary entry point for time-travel queries in Semantica, accepting ISO-8601 strings, datetime objects, or Unix epochs.
- The method relies on **`_normalize_timestamp()`** (lines 2510-2515) to ensure consistent UTC comparison logic.
- Temporal validity is enforced through **`is_active()`** methods on `ContextNode` (lines 333-350) and `ContextEdge` (lines 996-1013) using closed-interval bounds.
- Returned snapshots contain `nodes`, `edges`, and `decisions` arrays filtered to the query moment without mutating the underlying graph.
- This architecture enables immutable historical auditing, difference analysis between timestamps, and temporal provenance tracking.

## Frequently Asked Questions

### What timestamp formats does ContextGraph.state_at() accept?

According to the semantica-agi/semantica source code, the method accepts ISO-8601 formatted strings (e.g., `"2023-06-15T12:00:00Z"`), Python `datetime` objects (timezone-aware or naive), and Unix epoch integers representing seconds since 1970-01-01 UTC. The internal `_normalize_timestamp()` helper within [`context_graph.py`](https://github.com/semantica-agi/semantica/blob/main/context_graph.py) (lines 2510-2515) handles conversion to a standardized timezone-naive UTC datetime.

### Does calling state_at() modify the underlying graph?

No. The `state_at()` method is strictly read-only. It constructs a new serializable dictionary containing copies of active node and edge data without altering the in-memory graph structure. This immutability guarantee allows you to safely call the method multiple times to compare different historical states or generate audit trails.

### How does Semantica handle timezone-aware datetime objects?

The `_normalize_timestamp()` implementation converts all timezone-aware `datetime` objects to timezone-naive UTC before comparison. This ensures that `valid_from` and `valid_until` boundaries are evaluated consistently regardless of the input timezone, preventing off-by-hour errors in temporal queries.

### Can I use state_at() to detect changes between two points in time?

Yes. By capturing two snapshots at different timestamps and comparing their `nodes`, `edges`, and `decisions` arrays, you can compute temporal diffs. Because each snapshot returns the exact set of active elements at that moment, you can identify additions, removals, or modifications by comparing entity IDs and their validity windows between the two dictionaries.