> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cognee.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Fact Validity

> Close superseded facts with close_node() and filter stale ones with is_valid()

A minimal guide to closing superseded facts. When a fact changes ("Alice works at Acme" becomes "Alice works at Globex"), close the old node instead of deleting it — the graph keeps the history, and `is_valid()` tells you whether any node is still current.

## Before You Start

* Complete [Quickstart](/getting-started/quickstart) to understand basic operations
* Ensure you have [LLM Providers](/setup-configuration/llm-providers) configured
* Read [DataPoints](/core-concepts/building-blocks/datapoints) for the `valid_to` field and the rest of the node schema
* Use the default Ladybug [graph store](/setup-configuration/graph-stores) — currently the only backend where `close_node()` can persist the `valid_to` stamp (see [Backend Support](#backend-support))

## Code in Action

```python theme={null}
import asyncio

import cognee
from cognee.infrastructure.databases.graph import get_graph_engine
from cognee.modules.engine.models import Entity

# close_node and is_valid are not re-exported from cognee.tasks.storage,
# so import them from their module.
from cognee.tasks.storage.close_node import close_node, is_valid


async def main():
    # Step 1: start clean and remember the original fact.
    await cognee.forget(everything=True)
    await cognee.remember("Alice works at Acme.", dataset_name="employment_facts")

    # Step 2: entity node ids are deterministic — derive Acme's id from its name.
    acme_id = Entity.id_for("Acme")

    # Step 3: Alice changes jobs — close the old fact instead of deleting it.
    closed = await close_node(acme_id)
    print("closed:", closed)  # True — the node existed and valid_to was stamped

    # Step 4: remember the replacement fact.
    await cognee.remember("Alice works at Globex.", dataset_name="employment_facts")

    # Step 5: the closed node is still in the graph, just stale.
    graph = await get_graph_engine()
    node = await graph.get_node(str(acme_id))
    print("is_valid:", is_valid(node))  # False — the fact was superseded


if __name__ == "__main__":
    asyncio.run(main())
```

## What Just Happened

### Step 1: Remember the Original Fact

```python theme={null}
await cognee.forget(everything=True)
await cognee.remember("Alice works at Acme.", dataset_name="employment_facts")
```

`forget(everything=True)` starts from a clean slate, then `remember()` builds the fact into the knowledge graph the same way all your data gets in — no low-level ingestion needed. Every node it creates carries a `valid_to` stamp (`int | None`, ms epoch) that defaults to `None`, meaning the fact is still current.

### Step 2: Derive the Node Id from the Entity Name

```python theme={null}
acme_id = Entity.id_for("Acme")
```

`remember()` turned the sentence into entity nodes, and entity node ids are deterministic: `Entity.id_for(name)` applies the same normalization the ingestion pipeline uses (lowercase, spaces to underscores, apostrophes stripped) and returns the id the entity was stored under. No graph scan needed — this works the same on ten nodes or a hundred thousand.

### Step 3: Close the Superseded Fact

```python theme={null}
closed = await close_node(acme_id)
print("closed:", closed)  # True — the node existed and valid_to was stamped
```

`close_node()` stamps `valid_to` on the stored node — "now" by default — marking the fact superseded without deleting it. It returns `True` only if the node existed and was patched, and `False` otherwise (for example when the id is not in the graph). Note the import path: `from cognee.tasks.storage.close_node import close_node, is_valid`.

### Step 4: Remember the Replacement Fact

```python theme={null}
await cognee.remember("Alice works at Globex.", dataset_name="employment_facts")
```

The new fact flows in through `remember()` like any other data and becomes its own nodes. Supersede instead of delete: the old node stays in the graph with `valid_to` set, so your memory keeps the history of what used to be true.

### Step 5: Check Staleness with is\_valid()

```python theme={null}
graph = await get_graph_engine()
node = await graph.get_node(str(acme_id))
print("is_valid:", is_valid(node))  # False — the fact was superseded
```

`is_valid(node, at_ms=None)` returns `True` while `valid_to` is `None` (never closed) or lies strictly in the future relative to `at_ms` (default: now). It accepts either a `DataPoint` instance (reads the attribute) or a plain graph-node dict (reads the key), so it works on records read back from the graph engine, as here.

## Behavior to Know About

* **Node-level granularity.** `valid_to` lives on nodes: closing marks the whole node stale, and the edges attached to it are not stamped.
* **Backdating.** `close_node(node_id, at_ms=...)` stamps a specific ms-epoch timestamp instead of "now", and `is_valid(node, at_ms=some_past_ms)` asks whether the fact was still current at that moment.
* **Not idempotent.** Closing is last-write-wins: re-closing an already-closed node overwrites `valid_to` with the new timestamp (earlier or later). Guard with `is_valid()` first if you need the first close to stick.
* **Two time axes.** `valid_to` records when a fact *stopped being true*. It is not the `time_to` on `Interval` nodes (the time range an `Event` points to via `during`), which records when an event *occurred* — that axis belongs to [Time Awareness](/guides/time-awareness).
* **Retrieval is not filtered yet.** Search and graph completion neither filter nor down-weight closed nodes, so a superseded fact can still surface in results. Applying `is_valid()` to what you retrieve is currently the caller's job; retrieval-side consumption is planned as a follow-up.

## Backend Support

`close_node()` persists `valid_to` through the graph adapter's optional `update_node` method — see [Adding a new graph database](/contributing/adding-providers/adding-new-graph-database) for the adapter contract.

<Warning>
  Only the default Ladybug store implements `update_node` today. On every other backend (Neo4j, Kuzu, Postgres, Neptune, Turso), `close_node()` logs a warning and returns `False` — nothing is persisted, and no exception is raised. Check the return value rather than assuming the close landed.
</Warning>

<Columns cols={2}>
  <Card title="Time Awareness" icon="clock" href="/guides/time-awareness">
    The other time axis: extract events and timestamps and run time-aware queries.
  </Card>

  <Card title="DataPoints" icon="circle" href="/core-concepts/building-blocks/datapoints">
    The building block that carries `valid_to` and the rest of the node schema.
  </Card>
</Columns>
