Skip to main content
Once a graph has been ingested, real corpora keep changing: documents get edited, replaced, or removed. Re-running a full ingest on every change is expensive and discards the existing graph. The v1.1.0 incremental update primitives let you mutate an already-built graph in place, with crash-safe semantics and scoped orphan cleanup. This page is the API reference and usage guide for those primitives: The canonical use case is CI-driven graph updates on PR merge: a typical diff has all three change types in one go, and apply_changes() routes each list to the right primitive.

Mental Model — What Lives in the Graph

To understand the effect of each primitive, it helps to know the node shapes the lexical graph keeps: Incremental updates are scoped via these relationships — orphan cleanup never goes global, it only inspects entities the touched document actually referenced.

update()

Re-sync a previously-ingested document. The new content replaces the old chunks, entities are re-extracted, and entities that become orphaned (no remaining MENTIONED_IN edges) are cleaned up along with their incident RELATES edges.

Parameters

Returns — UpdateResult

Effect on the graph

Crash safety

The call uses a six-phase state machine with one load-bearing commit point (mark_pending_committed). Crashes before the commit roll back (pending data is discarded); crashes after the commit roll forward (cleanup resumes on the next call to update/delete_document against this id).

Example


delete_document()

Remove a single document and everything it uniquely owns.

Parameters

Returns — DeleteDocumentResult

Effect on the graph

Crash safety

A single atomic write (pending_delete=true + cleanup-state arrays on the doc) is the commit marker. Before that write the live document is untouched. After it, every remaining step is idempotent and resumes on the next call against this id.

Example


apply_changes()

Heterogeneous batch — the convenience wrapper for CI-driven incremental ingestion. Dispatches each list to the right primitive in a fixed order:
  1. deleteddelete_document()
  2. modifiedupdate(if_missing="ingest") (so a “modified” file the graph has never seen is upserted, not errored)
  3. addedingest()

Parameters

The order deletes → updates → adds is part of the public contract; do not assume reordering is safe. Overlapping ids across input lists raise ValueError at the input boundary — this is almost always a broken git-diff parser.

Returns — ApplyChangesResult

Each entry aligns by index with the corresponding input list. Per-file errors are wrapped as BatchEntry with error (string) and error_type (exception class name) set; the batch never raises. Branch on entry.is_success:

Effect on the graph

apply_changes() does not itself touch the graph — it dispatches to the primitives documented above and aggregates their results. Two whole-batch consequences worth knowing:
  • No automatic finalize(). Cross-document deduplication is O(graph size), so the batch deliberately leaves it to the caller. Call finalize() once after the batch, not once per file.
  • Peak entity cardinality is minimised because deletes run first — orphan candidates are gone before adds can re-introduce overlapping ids.

Canonical CI usage

Concurrency and the update_concurrency trap

Raising update_concurrency above 1 is unsafe in general because the orphan-cleanup correctness proof relies on MENTIONED_IN edges being persisted before any cutover begins. Two concurrent updates sharing an entity e1 are only guaranteed to preserve it because:
  • Pre-cutover, e1 still has its old MENTIONED_IN edges.
  • Post-pipeline.run() (but pre-cutover), the new MENTIONED_IN edges are already written.
So an update doing orphan cleanup will always see at least one incident edge. Raising the default to ≥2 is only safe if you can independently guarantee that no two parallel updates can share an entity in their candidate snapshots. The integration test test_concurrent_updates_preserve_shared_entity is the tripwire for this invariant — break it before bumping the value.

finalize()

Run once after a batch of ingests/updates/deletes. Skipping it leaves cross-document duplicates in place and disables entity-/edge-level vector search.

Returns — FinalizeResult

What it does (in order)

  1. Removes NULL-name stub entities (legacy cleanup).
  2. deduplicate_entities() — global exact-name dedup across all documents.
  3. backfill_entity_embeddings() — embeds entity names that have no embedding yet (incremental-safe).
  4. embed_relationships() — embeds fact text on RELATES edges.
  5. ensure_indices() — rebuilds/verifies all indexes.
Steps 2 and 3 are why apply_changes() does not call it automatically: they scan the whole graph and would re-run on every file in a batch.

Choosing the Right Primitive


End-to-End Example

A full runnable example lives in graphrag_sdk/examples/07_incremental_updates.py. It exercises:
  • Initial ingest with a stable document_id
  • No-op update (content-hash short-circuit)
  • Real update with orphan cleanup
  • Adding a second document
  • A heterogeneous apply_changes() batch
  • A single trailing finalize()
  • Verifying with completion()

See Also

  • Ingestion — the underlying 9-step pipeline that ingest() and update() both drive.
  • Storage — node/edge shapes, including why MERGE makes re-ingestion idempotent.
  • Configurationfinalize() reference and post-ingestion knobs.
  • Ontology Discovery — if you’re updating against a corpus and don’t have a schema yet, or want to propose schema additions from new documents via suggest_schema_extensions.
  • Ontology Evolution — if your schema needs to change between updates (add_entity, add_attribute, rename_*, etc.) — these are the mutation primitives that the discovery proposal hands off to.