DEV Community

Cover image for Temporal Edges in Knowledge Graphs: Why Static Edges Break Graph RAG
Tae Kim
Tae Kim

Posted on • Originally published at hannune.ai

Temporal Edges in Knowledge Graphs: Why Static Edges Break Graph RAG

Standard knowledge graph edges are timeless by default. When you insert "Company X → CEO → Person A," there is no expiration date on that relationship. The graph becomes stale the moment the CEO changes, but it does not know it is stale.

I ran into this building a multilingual knowledge graph for East Asian corporate data at 2asy.ai. Corporate structures change constantly — leadership transitions, subsidiary changes, regulatory status updates — and every change makes some existing edges incorrect rather than just outdated.

The Problem with Static Edges

Here is what a static edge insertion looks like:

# Static edge — no temporal metadata
graph.add_edge(
    source="samsung-electronics",
    relation="CEO",
    target="lee-jae-yong",
)
Enter fullscreen mode Exit fullscreen mode

When the CEO changes, you either:

  • Delete the old edge and insert a new one (losing history)
  • Insert a new edge without removing the old one (getting conflicting answers)

Neither is acceptable when you need the graph to answer both current and historical queries correctly.

The Fix: (valid_from, valid_to) on Every Edge

Every relationship gets temporal properties as first-class fields:

# Temporal edge
graph.add_edge(
    source="samsung-electronics",
    relation="CEO",
    target="lee-jae-yong",
    valid_from="2022-10-27",
    valid_to=None,  # None = currently active
)
Enter fullscreen mode Exit fullscreen mode

When an event modifies an existing relationship, the old edge gets a closed valid_to date rather than being deleted:

def supersede_ceo(company_id: str, new_ceo_id: str, effective_date: str):
    current = graph.find_edge(
        source=company_id,
        relation="CEO",
        valid_to=None
    )
    if current:
        graph.update_edge(current.id, valid_to=effective_date)

    graph.add_edge(
        source=company_id,
        relation="CEO",
        target=new_ceo_id,
        valid_from=effective_date,
        valid_to=None,
    )
Enter fullscreen mode Exit fullscreen mode

Graph traversal queries default to as-of the current date unless the caller explicitly requests full history.

The Hard Part: Supersession Logic

The tricky design decision is distinguishing which edge types supersede and which accumulate.

A CEO appointment is one-to-one: the new edge invalidates the old one. A subsidiary relationship is many-to-many: a company can have multiple subsidiaries simultaneously, so the new edge extends rather than replaces.

This distinction has to live in the edge schema, not in the insertion code.

EDGE_CARDINALITY = {
    "CEO": "one_to_one",
    "CFO": "one_to_one",
    "SUBSIDIARY": "many_to_many",
    "BOARD_MEMBER": "many_to_many",
}
Enter fullscreen mode Exit fullscreen mode

If it lives in insertion code, every developer who writes an insertion path has to know the business rules. If it lives in the schema, the graph enforces it automatically.

For East Asian corporate data, I had to map over 40 relationship types to cardinality rules. The initial list took two days; maintaining it as new event types were discovered was ongoing work.

Graph RAG Implications at Retrieval Time

A query for "Samsung's leadership structure in Q3 2024" needs to traverse edges that were valid during that specific window:

def query_as_of(start_node_id: str, relation: str, as_of_date: str):
    return graph.find_edges(
        source=start_node_id,
        relation=relation,
        filters={
            "valid_from": {"lte": as_of_date},
            "valid_to": {"gte": as_of_date, "or_null": True}
        }
    )
Enter fullscreen mode Exit fullscreen mode

Without temporal modeling, the graph returns the current structure and nothing in the response signals that it is answering a different question than the one asked.

Why Test Sets Hide This Problem

The retrieval accuracy difference between a temporally-modeled graph and a static graph is not visible on test sets built from current data. It shows up the first time a user asks a historical question and the system answers confidently with the wrong year's data.

Standard RAG evaluation benchmarks almost always test against current-state questions. If your knowledge domain has history that matters — corporate actions, regulatory changes, personnel movements — you have to build your own eval set with historical queries and known-correct answers at specific timestamps.

I built ours from public Korean corporate disclosure data, which has machine-readable effective dates on most events. That gave us a dataset where we knew what the correct graph state was at any given quarter. The temporal modeling errors were the largest single category before we added (valid_from, valid_to) to the schema.


Working on multilingual knowledge graphs and Graph RAG pipelines at 2asy.ai. More writing at hannune.ai.

Cover: Planet Volumes on Unsplash

Top comments (0)