Skip to main content

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:
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:
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)
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?”

Why Bi-Temporality Matters

Handling Contradictions

When new information contradicts existing knowledge, Graphiti invalidates old edges without deleting them:
# 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
Graphiti uses temporal edge invalidation rather than deletion, preserving the complete history of what the system knew and when.

Late-Arriving Information

Bi-temporality elegantly handles information that arrives out of chronological order:
# 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:
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:
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:
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:
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
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):
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:
# 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)
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.

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

# 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

# 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:
# 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

FieldSourceCode Location
valid_atreference_time in add_episode()graphiti_core/graphiti.py:918
created_atutc_now() when episode is processedgraphiti_core/graphiti.py:876
invalid_atExtracted from episode content or set during invalidationgraphiti_core/utils/maintenance/edge_operations.py
expired_atSet when edge is invalidated by newer informationDeduplication logic

Date Parsing

Graphiti uses parse_db_date() to handle various database date formats:
# 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

# 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:
results = await graphiti.search(
    "company leadership",
    filters=SearchFilters(valid_time=historical_date)
)

3. Preserve Episode Metadata

When processing documents, preserve original timestamps:
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

SystemValid TimeTransaction TimeInvalidationPoint-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
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.

Next Steps

Episodes

Learn how episodes carry temporal information

Nodes and Edges

Explore the complete graph schema

Search & Retrieval

Use temporal filters in search queries

Add Episodes

Set reference_time correctly when adding data

Build docs developers (and LLMs) love