cognee.run_migrations
Applies all pending database schema migrations before the rest of your application starts.In releases before Cognee 1.5.0 this function was named
run_startup_migrations. That name is gone — update calls to cognee.run_migrations() when you upgrade.- Relational schema — executes
alembic upgrade headagainst your configured relational database (SQLite by default, or Postgres).- Revisions
c5d7e9f1a3b5andd6e8f0a2b4c6complete the dataset-scoping of thedatatable — see Dataset-scoping upgrade below. - This step also heals the SQL session-cache table on existing deployments. Revision
c3d5e7f9a1b2deletes all but the newest row per(user_id, session_id, entry_id)incache_session_contextand creates theuq_cache_session_context_entryunique index that the cache adapter’s upsert targets. It covers both the database Alembic is connected to and the standalone SQLitecache.dbthatCACHE_BACKEND=sqlite(the default) keeps next to the relational database. Cache tables are created on init rather than managed by Alembic, so fresh databases already get the index from the table definition and only pre-existing tables are touched. Because the migration mutates data and builds a unique index, it can take time and hold locks on a largecache_session_contexttable — back up the database and run it in a maintenance window.
- Revisions
- Vector schema — runs the vector adapter’s
run_migrationsmethod for every database that needs it:- Single-user mode (
ENABLE_BACKEND_ACCESS_CONTROL=False): migrates the single default vector engine. - Multi-user mode (
ENABLE_BACKEND_ACCESS_CONTROL=True, the default): iterates over every dataset database and migrates each one individually. A failure for one dataset is logged and skipped; the remaining datasets continue to migrate. - If the active vector engine has no
run_migrationsmethod, Cognee logs a warning and skips that engine. - If the
dataset_databasetable does not exist yet (a fresh database), the vector migration step is skipped with a warning instead of raising. This is handled on both SQLite (OperationalError, “no such table”) and PostgreSQL/pgvector (ProgrammingError/UndefinedTableError).
- Single-user mode (
Migrations that re-embed batch their embedding requests. Some data migrations re-embed rows through
cognify’s indexing path; those embeddings are sent in batches, so a large migration no longer fails on provider request-size or rate limits. Batching follows the same EMBEDDING_BATCH_SIZE and EMBEDDING_MAX_CONCURRENT_DATA_POINTS settings as normal indexing; there is no migration-specific setting to tune.Dataset-scoping upgrade
Cognee 1.5.0 makesData rows dataset-scoped: a row belongs to exactly one dataset, and the same content added to two datasets is two independent rows (see deduplication). Databases created by an earlier release stored one row shared across datasets through a dataset_data membership table, so upgrading needs a data migration, not just a schema change.
Two Alembic revisions do the relational half:
The backfill splits rows per dataset:
- One membership (the overwhelming majority) — the row keeps its original id and is stamped with that dataset, so data ids you stored externally stay valid.
- Several memberships — the oldest membership keeps the original id; every other dataset gets a fresh row with a new id, all columns copied, and
legacy_idrecording the pre-split id. Relational ledger rows for those datasets are repointed to the new ids. - No memberships — unreachable orphans, left untouched.
legacy_id is preserved, every id ever issued keeps resolving within its dataset: Cognee matches the exact id first, then falls back to legacy_id.
The graph and vector stores are then brought in line by the rekey_fork_document_ids migration in Cognee’s data-migration chain, which runs after the relational step. For each split (“fork”) document it re-keys the graph document node to the canonical relational id, updates the ledger’s node/edge references, refreshes the document_id property on the document’s chunk nodes, and re-upserts the chunks’ vector index rows — in batched embedding requests, as described above — so their document_id payload cites the canonical id. Chunk point ids are unchanged. Fork rows are rare — same user, identical content, several datasets, all before the upgrade — so on most deployments this is one indexed relational query and no per-document work.
When a fork does exist on a large graph, the graph half of that re-key is real work. Its edge-identity and node-id lookups run as chunked index seeks and its provenance restore is attached in batches grouped by pipeline run, so the re-key completes within the worker subprocess deadline instead of timing out — but on a graph of ~100k nodes and ~300k edges it can still run for tens of minutes. On the default embedded Ladybug backend (this does not apply to Neo4j or Postgres graph stores, which have no such cap), give the store headroom under its size cap — the kuzu_max_db_size setting (KUZU_MAX_DB_SIZE) — before starting, and let the run finish: killing a worker mid-checkpoint can leave .lbug.shadow / .wal.checkpoint recovery files behind that block the next open, and repeated interrupted runs can exhaust the cap even while the database is small on disk.
Much of that graph work is set-based rather than row-by-row. On Ladybug/Kuzu, the provenance move is one replace() statement over the nodes and one over the edges still carrying the pre-fork key, so the batched restore described above handles only the residue: artifacts that already carry both keys are deliberately left to that generic attach-then-remove sweep, which dedupes them, so the migration still converges on re-run. Every other graph backend — and any engine that rejects those statements — takes the unchanged generic path for the whole move, and the migrated result is identical either way. The survivor-edge repair in the sibling namespace_entity_type_node_ids migration is trimmed the same way on any backend that can list its edges in one scan: it asks which at-risk edges were actually dropped and re-asserts only those, and blind-upserts every at-risk edge only where that scan isn’t answerable. On a large fork subgraph these shortcuts make the migration substantially faster.
Both migrations are reversible, but the rollback order is the inverse of the upgrade order: run the rekey_fork_document_ids downgrade before the d6e8f0a2b4c6 downgrade. The reverse map is built from legacy_id, so dropping that column first strands fork graphs on canonical ids with no way back. Downgrading also loses fork lineage permanently — the old schema has nowhere to keep it — so a later re-upgrade re-splits shared rows with fresh ids.
Entrypoints
All migration functions live in thecognee.run_migrations module. run_migrations is also re-exported at the top level as cognee.run_migrations, so it is the recommended entrypoint for most applications.
When to call it
For the default local setup (SQLite + LanceDB), Cognee handles migrations automatically when the API server starts. You only need to call
run_migrations() explicitly in server deployments or CI pipelines where you manage database lifecycle yourself.Disabling automatic migrations
run_migrations() is called for you in a few places: the FastAPI server’s startup lifespan, the first cognify() or remember() call in a process, and — in the Docker image — the container entrypoint before the server binds its port.
Set ENABLE_AUTO_MIGRATIONS=false to disable all of those automatic runs:
true. It is disabled by false, 0, or no (case-insensitive); any other value leaves automatic migrations on. When disabled, run_migrations() logs that it was skipped and returns an empty list without touching any database.
For the container-startup side of this flag, see Docker → Database Migrations on Startup.
Example
Kubernetes init container
Run migrations as a one-shot init container so the main pod only starts after the schema is ready:Concurrency
Every migration flow runs under a single cross-process lock, so a host performs at most one migration of any kind at a time. If several processes start at once — multiple workers of the same server, parallel SDK runs, or several init containers — only one acquires the lock and migrates; the others block until it finishes, then re-read the stored revision and skip work that is already done. Nothing runs migrations in parallel. Because of this, startup can block (and time-to-ready can increase) while another process holds the lock and migrates. This is expected under the Kubernetes init container and multi-worker scenarios above — the wait is the coordination working as intended, not a hang. The lock backend depends on your relational database:- Postgres — a session-scoped advisory lock, which also serializes migrations across hosts. Use Postgres metadata when multiple hosts may start and migrate at the same time.
- SQLite — an OS advisory file lock placed next to the database file. It serializes multiple processes on a single host (multi-worker servers, parallel SDK runs) but not across hosts or over NFS.
Errors
Related
- Relational Databases — configure SQLite or Postgres
- Deployment Overview — how to structure Cognee in production
- Kubernetes (Helm) — full Kubernetes deployment guide