> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/getzep/graphiti/llms.txt
> Use this file to discover all available pages before exploring further.

# Bi-Temporal Data Model

> How Graphiti tracks both real-world time and system time for accurate historical reasoning

## Introduction to Temporal Knowledge

Most knowledge graphs treat time as a simple timestamp—when a fact was added to the database. Graphiti takes a more sophisticated approach with a **bi-temporal model** that distinguishes between:

1. **Valid Time**: When a fact was true in the real world
2. **Transaction Time**: When the system learned about the fact

This dual perspective enables accurate point-in-time queries and proper handling of late-arriving information.

## The Bi-Temporal Model

Graphiti implements bi-temporality through two sets of timestamps on edges and nodes:

### Valid Time Dimension

Tracks when facts were **actually true** in the real world:

```python theme={null}
class EntityEdge(Edge):
    valid_at: datetime | None      # When the fact became true
    invalid_at: datetime | None    # When the fact stopped being true
```

**Example**: "Kamala Harris served as California Attorney General from 2011 to 2017"

* `valid_at`: January 3, 2011
* `invalid_at`: January 3, 2017

### Transaction Time Dimension

Tracks when the system **learned about** facts:

```python theme={null}
class EntityEdge(Edge):
    created_at: datetime           # When the edge was first created
    expired_at: datetime | None    # When the edge was superseded/invalidated
```

**Example**: You add the Attorney General fact on March 15, 2025

* `created_at`: March 15, 2025
* `expired_at`: None (edge is still current in the graph)

<Info>
  The distinction between valid time and transaction time allows Graphiti to answer questions like "What did the system know about X on date Y?" versus "What was actually true about X on date Y?"
</Info>

## Why Bi-Temporality Matters

### Handling Contradictions

When new information contradicts existing knowledge, Graphiti invalidates old edges without deleting them:

```python theme={null}
# Initial knowledge (added on March 1, 2025)
result1 = await graphiti.add_episode(
    name="News Article",
    episode_body="Alice is the CEO of Acme Corp.",
    reference_time=datetime(2023, 1, 1, tzinfo=timezone.utc),
)
# Edge created with:
# - valid_at: 2023-01-01 (when Alice became CEO)
# - created_at: 2025-03-01 (when we learned this)
# - expired_at: None

# Later information (added on March 15, 2025)
result2 = await graphiti.add_episode(
    name="Company Update",
    episode_body="Bob became CEO of Acme Corp on January 1, 2024.",
    reference_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
)
# Old edge updated:
# - invalid_at: 2024-01-01 (Alice's tenure ended)
# - expired_at: 2025-03-15 (when the system learned this)
# New edge created for Bob with valid_at: 2024-01-01
```

<Note>
  Graphiti uses **temporal edge invalidation** rather than deletion, preserving the complete history of what the system knew and when.
</Note>

### Late-Arriving Information

Bi-temporality elegantly handles information that arrives out of chronological order:

```python theme={null}
# You learn about a 2020 event in 2025
await graphiti.add_episode(
    name="Old Email Found",
    episode_body="Alice joined Acme Corp in 2020.",
    reference_time=datetime(2020, 6, 15, tzinfo=timezone.utc),
)
# Edge created with:
# - valid_at: 2020-06-15 (when it actually happened)
# - created_at: 2025-03-15 (when the system ingested it)
```

This separation allows:

* **Historical accuracy**: Query "Who worked at Acme in 2020?" → Correct answer: Alice
* **Audit trails**: Query "What did we know on March 1, 2025?" → We didn't know about Alice yet

## Temporal Queries

Graphiti's temporal model enables sophisticated queries:

### Point-in-Time Retrieval

Find facts that were valid at a specific time:

```python theme={null}
from graphiti_core.search.search_filters import SearchFilters
from datetime import datetime, timezone

# Who was CEO in 2023?
results = await graphiti.search(
    query="CEO of Acme Corp",
    filters=SearchFilters(
        valid_time=datetime(2023, 6, 1, tzinfo=timezone.utc)
    )
)
# Returns only edges where:
# valid_at <= 2023-06-01 AND (invalid_at > 2023-06-01 OR invalid_at IS NULL)
```

### Time Range Queries

Find all facts valid during a period:

```python theme={null}
from graphiti_core.search.search_filters import SearchFilters

results = await graphiti.search(
    query="employment at Acme Corp",
    filters=SearchFilters(
        valid_time_start=datetime(2020, 1, 1, tzinfo=timezone.utc),
        valid_time_end=datetime(2023, 12, 31, tzinfo=timezone.utc)
    )
)
```

### Recent Updates Query

Find what the system learned recently:

```python theme={null}
from datetime import datetime, timedelta, timezone

# Get facts added in the last 7 days
results = await graphiti.search(
    query="Acme Corp",
    filters=SearchFilters(
        created_after=datetime.now(timezone.utc) - timedelta(days=7)
    )
)
```

## Temporal Data on Nodes

### Episodic Nodes

Episodes have both dimensions of time:

```python theme={null}
class EpisodicNode(Node):
    valid_at: datetime      # When the episode content occurred/was created
    created_at: datetime    # When it was ingested into Graphiti
```

**Example**: Processing a 2020 email in 2025

```python theme={null}
episode = EpisodicNode(
    name="Archived Email",
    content="Meeting notes from June 2020",
    source=EpisodeType.text,
    valid_at=datetime(2020, 6, 15, tzinfo=timezone.utc),  # Email date
    created_at=datetime(2025, 3, 15, tzinfo=timezone.utc), # Ingestion date
)
```

### Entity Nodes

Entities only track transaction time (when first created):

```python theme={null}
class EntityNode(Node):
    created_at: datetime    # When the entity was first added to the graph
```

Why? Because entities represent persistent objects that can have multiple changing relationships over time. The temporal validity is tracked at the **edge level**, not the node level.

## Episode Time vs Edge Time

Understanding the relationship between episode time and edge time is crucial:

```python theme={null}
# Episode: A podcast recorded on January 15, 2024
episode_result = await graphiti.add_episode(
    name="Tech Podcast #42",
    episode_body="Alice announced she's joining Acme Corp as CTO.",
    reference_time=datetime(2024, 1, 15, tzinfo=timezone.utc),  # Podcast date
    # Processed on March 15, 2025
)

# Extracted edge:
# - fact: "Alice is the CTO of Acme Corp"
# - valid_at: 2024-01-15 (inherited from episode.valid_at)
# - created_at: 2025-03-15 (when Graphiti processed it)
```

<Tip>
  The `reference_time` parameter in `add_episode()` becomes the `valid_at` timestamp for extracted edges. This ensures that facts are temporally anchored to when they occurred, not when they were processed.
</Tip>

## Temporal Edge Invalidation

When Graphiti detects contradictory information, it uses temporal invalidation:

### How It Works

1. **New episode arrives** with contradicting information
2. **LLM analyzes** the contradiction during edge resolution (see `graphiti_core/utils/maintenance/edge_operations.py:484`)
3. **Old edge is updated**:
   * `invalid_at` ← new fact's `valid_at`
   * `expired_at` ← current timestamp
4. **New edge is created** with the updated information

### Code Example from Source

```python theme={null}
# From graphiti_core/edges.py:271-279
class EntityEdge(Edge):
    expired_at: datetime | None = Field(
        default=None, description='datetime of when the node was invalidated'
    )
    valid_at: datetime | None = Field(
        default=None, description='datetime of when the fact became true'
    )
    invalid_at: datetime | None = Field(
        default=None, description='datetime of when the fact stopped being true'
    )
```

### Real-World Example

```python theme={null}
# March 1: Add initial fact
await graphiti.add_episode(
    name="Company Announcement",
    episode_body="Sarah is the VP of Engineering at TechCo.",
    reference_time=datetime(2022, 1, 1, tzinfo=timezone.utc),
)
# Edge: Sarah --[VP of Engineering]--> TechCo
#   valid_at: 2022-01-01, invalid_at: None, expired_at: None

# March 15: Add contradicting fact
await graphiti.add_episode(
    name="LinkedIn Update",
    episode_body="Sarah joined Acme Corp as CEO in January 2024.",
    reference_time=datetime(2024, 1, 1, tzinfo=timezone.utc),
)
# Old edge updated:
#   valid_at: 2022-01-01, invalid_at: 2024-01-01, expired_at: 2025-03-15
# New edge created:
#   valid_at: 2024-01-01, invalid_at: None, expired_at: None
```

## Temporal Awareness in Retrieval

Graphiti's search automatically considers temporal validity:

```python theme={null}
# Without temporal filter: Returns current facts
results = await graphiti.search("Who is the CEO?")
# Returns: Bob (most recent valid edge)

# With temporal filter: Returns historical facts
results = await graphiti.search(
    "Who is the CEO?",
    filters=SearchFilters(
        valid_time=datetime(2023, 6, 1, tzinfo=timezone.utc)
    )
)
# Returns: Alice (who was CEO at that time)
```

## Implementation Details

### Timestamp Sources

| Field        | Source                                                    | Code Location                                        |
| ------------ | --------------------------------------------------------- | ---------------------------------------------------- |
| `valid_at`   | `reference_time` in `add_episode()`                       | `graphiti_core/graphiti.py:918`                      |
| `created_at` | `utc_now()` when episode is processed                     | `graphiti_core/graphiti.py:876`                      |
| `invalid_at` | Extracted from episode content or set during invalidation | `graphiti_core/utils/maintenance/edge_operations.py` |
| `expired_at` | Set when edge is invalidated by newer information         | Deduplication logic                                  |

### Date Parsing

Graphiti uses `parse_db_date()` to handle various database date formats:

```python theme={null}
# From graphiti_core/helpers.py
def parse_db_date(db_date: Any) -> datetime | None:
    """Parse datetime from different database formats"""
```

This ensures consistent temporal handling across Neo4j, FalkorDB, Kuzu, and Neptune backends.

## Best Practices

### 1. Always Set reference\_time Accurately

```python theme={null}
# Good: Use the actual event time
await graphiti.add_episode(
    name="Historical Document",
    episode_body=document_text,
    reference_time=document_creation_date,  # When event occurred
)

# Bad: Using current time for historical data
await graphiti.add_episode(
    name="Historical Document",
    episode_body=document_text,
    reference_time=datetime.now(timezone.utc),  # Wrong!
)
```

### 2. Use Temporal Filters for Historical Queries

When you need point-in-time information, always use `SearchFilters`:

```python theme={null}
results = await graphiti.search(
    "company leadership",
    filters=SearchFilters(valid_time=historical_date)
)
```

### 3. Preserve Episode Metadata

When processing documents, preserve original timestamps:

```python theme={null}
episode = {
    'content': email_body,
    'source': EpisodeType.text,
    'reference_time': email_sent_date,  # Original email date
    'source_description': f"Email from {sender} at {email_sent_date}"
}
```

## Comparison with Other Systems

| System        | Valid Time | Transaction Time | Invalidation  | Point-in-Time Queries |
| ------------- | ---------- | ---------------- | ------------- | --------------------- |
| **Graphiti**  | ✅ Yes      | ✅ Yes            | ✅ Temporal    | ✅ Yes                 |
| **GraphRAG**  | ❌ No       | ✅ Yes            | ❌ No          | ❌ No                  |
| **Neo4j**     | 🟡 Manual  | ✅ Yes            | 🟡 Manual     | 🟡 With custom schema |
| **Vector DB** | ❌ No       | ✅ Yes            | ❌ Delete only | ❌ No                  |

<Tip>
  Graphiti's bi-temporal model is particularly valuable for AI agents that need to reason about **how knowledge evolved over time** and handle **late-arriving or contradictory information** gracefully.
</Tip>

## Next Steps

<CardGroup cols={2}>
  <Card title="Episodes" icon="book" href="/concepts/episodes">
    Learn how episodes carry temporal information
  </Card>

  <Card title="Nodes and Edges" icon="diagram-project" href="/concepts/nodes-and-edges">
    Explore the complete graph schema
  </Card>

  <Card title="Search & Retrieval" icon="magnifying-glass" href="/guides/search">
    Use temporal filters in search queries
  </Card>

  <Card title="Add Episodes" icon="plus" href="/guides/add-episodes">
    Set reference\_time correctly when adding data
  </Card>
</CardGroup>
