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:
Valid Time: When a fact was true in the real world
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.
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?”
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.
Bi-temporality elegantly handles information that arrives out of chronological order:
# You learn about a 2020 event in 2025await 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
from graphiti_core.search.search_filters import SearchFiltersfrom 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)
from datetime import datetime, timedelta, timezone# Get facts added in the last 7 daysresults = await graphiti.search( query="Acme Corp", filters=SearchFilters( created_after=datetime.now(timezone.utc) - timedelta(days=7) ))
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.
Understanding the relationship between episode time and edge time is crucial:
# Episode: A podcast recorded on January 15, 2024episode_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.
# From graphiti_core/edges.py:271-279class 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' )
# March 1: Add initial factawait 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 factawait 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
# Without temporal filter: Returns current factsresults = await graphiti.search("Who is the CEO?")# Returns: Bob (most recent valid edge)# With temporal filter: Returns historical factsresults = 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)
# Good: Use the actual event timeawait graphiti.add_episode( name="Historical Document", episode_body=document_text, reference_time=document_creation_date, # When event occurred)# Bad: Using current time for historical dataawait graphiti.add_episode( name="Historical Document", episode_body=document_text, reference_time=datetime.now(timezone.utc), # Wrong!)
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}"}
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.