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

The surviving entities’ descriptions are rewritten: what this document’s chunks said about them is taken off, unless another chunk still says it (no LLM call). A summarised entity cannot have text taken out of its summary: when the removed text is no longer said by any remaining edge, it is rebuilt from its remaining MENTIONED_IN edges if every one of them was written by this version (said_version; a table row’s edge does not count), and otherwise its summary is kept and the removed text beside it is still dropped. An unchanged chunk keeps its entities’ summaries for sure only with cache_unchanged_chunks=True; without it the chunk is extracted again, and a summary is kept only if the model describes the entity in the same words. An entity still holding more than five descriptions is summarised by the next finalize(). update() does the same for the chunks a new version replaces. Text a document ingested by an earlier SDK version wrote cannot be told apart, and is kept.

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. deleted → delete_document()
  2. modified → update(if_missing="ingest") (so a “modified” file the graph has never seen is upserted, not errored)
  3. added → ingest()

Parameters

The order deletes → updates → adds is part of the public contract; do not assume reordering is safe. Relationship tables (a RelationshipMapping in the ontology) in added or modified run after all of it, modified first, because their endpoints resolve against nodes the entity tables in the same batch write. A git diff that lists consumed_batch.csv before unit.csv still writes every edge, and each entry keeps its position in the result. 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(). Call finalize() once after the batch, not once per file. Its dedup checks only the entities whose check is pending — the ones the batch added, which have no name vector yet, and any an earlier run left pending — so one call covers the whole batch and a second call with nothing pending does no dedup work (an entity whose check did not complete stays pending and is retried). update() and deletes may run while finalize() deduplicates; a merge whose entities they change is skipped and decided again on the next run.
  • 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() — incremental: the pending entities (added since the last finalize(), or left pending by it) are checked against the whole graph (exact-name and acronym match, then one judge pass over their nearest names). Dedup also writes every entity’s name embedding. Then every entity holding more than five descriptions gets one LLM summary of them (descriptions_summarised); one whose summary fails keeps them and is listed in description_summary_failures, to be tried again by the next run.
  3. embed_relationships() — embeds fact text on RELATES edges.
  4. ensure_indices() — rebuilds/verifies all indexes.
apply_changes() does not call it automatically: one finalize() after the batch judges all of the batch’s new entities together, in fewer LLM calls than one per file.

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.
  • Configuration — finalize() 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.