Adding a New Graph Database to cognee
This guide describes how to integrate a new graph database engine into cognee.Repository Options
Cognee used both the core and the community repositories to host graph-database adapters. 🚨 From now on every graph-database adapter – except Kuzu – will live in the cognee-community repository.NetworkX has already been migrated and the remaining adapters will follow shortly.
Therefore all new adapter contributions must target the community repository.The core repository will keep only the built-in Kuzu integration.
For Community Repository
To add a new adapter to cognee-community:- Fork and clone the cognee-community repository, and branch from
main— unlike the core Cognee repo, cognee-community has nodevbranch. - Create your adapter in
packages/<engine_name>/cognee_community_graph_adapter_<engine_name>/ - Inside that directory add
__init__.py,<engine_name>_adapter.py, andregister.py(see the Redis example). - At the package root
packages/<engine_name>/add__init__.py,pyproject.toml, andREADME.md. - Run the shared graph conformance suite in
packages/shared/contract_suite/graph_contract.pyagainst your adapter. - Submit a pull request to the community repository
Why cognee-community?
cognee-community is the extension hub for Cognee.Anything that is not part of the core lives here—adapters for third-party databases, pipelines, community contributed additional tasks, etc.
Placing your adapter in this repository means:
- Your code is released under the community license and can evolve independently of the core.
- It can be installed with
pip install cognee-community-graph-adapter-(engine_mane)without pulling in heavyweight drivers for users who don’t need them. For example, for NetworkX it ispip install cognee-community-graph-adapter-networkx - These packages can be called with cognee core package using the registration step described below.
packages/* in the community repo—each sub-folder represents a separate provider implemented in exactly the way you are about to do.
1. Implement the Adapter
File: packages/graph/<engine_name>/cognee_community_graph_adapter_<engine_name>/<engine_name>_adapter.py
Your adapter must subclass GraphDBInterface, implementing all required CRUD and utility methods (e.g., add_node, add_edge, extract_node, etc.). Here is a sample skeleton with placeholders:
GraphDBInterface. Reference the KuzuAdapter or the Neo4jAdapter for a more comprehensive example.
Adapter instance reuse: Cognee’s graph engine factory caches adapter instances keyed by their configuration parameters. Multiple calls with identical settings return the same adapter object. Design your adapter to be safe for reuse — avoid per-instance mutable state that cannot be safely shared, and prefer lazy or thread-safe initialization where state is required. When an entry is evicted (e.g. cache eviction orcache_clear), the factory calls your adapter’sclose(). For capacity eviction the close is deferred until every leased reference to that instance is released, but the dataset-queue teardown and the idle reaper force-close immediately even while idle references are still held (holders transparently re-resolve to a fresh instance on next use) — so implementclose()idempotently and safe to run while stale references remain; if it raises, the error is logged and swallowed rather than propagated to the caller.
Optionalget_id_filtered_graph_data: This method is not part ofGraphDBInterface, so it is optional. If you implement it, graph-completion searches project only the vector-search neighborhood instead of loading the full graph viaget_graph_data(); if you omit it, Cognee falls back toget_graph_data(). The contract is edge-driven and matches the built-in Ladybug, Neo4j, and Postgres adapters: giventarget_ids, return(nodes, edges)whereedgesare every edge with either endpoint intarget_ids, andnodesare all endpoint nodes of those edges (same(node_id, properties)/(source_id, target_id, relationship_label, properties)shapes asget_graph_data()). Return([], [])whentarget_idsis empty; Cognee also falls back to the full graph if the filtered result comes back empty.
Declaring Cypher support:GraphDBInterfacedeclaressupports_cypher_queries: bool = True, so adapters are assumed to speak Cypher throughquery(). Override it toFalseon your adapter class whenquery()executes something else — the built-in Postgres and Turso adapters do this because theirquery()runs SQL against the graph tables.SearchType.CYPHERandSearchType.NATURAL_LANGUAGEthen raiseSearchTypeNotSupported(naming your adapter class) instead of handing your backend a Cypher string it cannot parse. Keep the flag on the class rather than setting it in__init__: tests and tooling read the capability directly off the adapter class without instantiating it (no database connection needed), as Cognee’s own adapter tests do.
Optionalget_triplets_batch— order before you paginate:GraphDBInterface.get_triplets_batch(offset, limit)is an optional extension; the base implementation raisesNotImplementedError, and only adapters that override it can back Memify’s triplet-embedding pipeline (the built-in Ladybug, Neo4j, Postgres, and Turso adapters do). If you implement it, your query must apply a total ordering before the offset/limit, becauseget_triplet_datapointsreads the whole graph with a single offset loop, advancing the offset by each batch’s size until a batch comes back short or empty, and that loop is only exhaustive if every call slices the same stable sequence. Paginating an unordered result set silently skips and duplicates rows. Sort on(source node id, target node id, relationship name)to match the built-in adapters. In Cypher this means puttingORDER BYin aWITHclause ahead ofSKIP/LIMITrather than afterRETURN, so the skip applies to an already-ordered stream.
Chunk bulk writes: Do not send an entireadd_nodes/add_edgespayload as one statement — a large ingest (tens of thousands of nodes or edges) can then exceed a per-statement or per-call deadline and never finish. Define a module-level_WRITE_CHUNK_SIZEand loop over the rows in slices of that size, issuing one statement per chunk; the built-in adapters use 2000 (Ladybug), 1000 (Postgres), and 500 (Turso), so pick a bound that suits your backend. Write each chunk as an idempotentMERGE/upsert: chunks are separate statements, so a run can fail partway through, and idempotent writes plus Cognee’s pipeline rollback ledger make the partial progress safe to re-apply. Chunking is internal to the adapter and does not change how many data points the pipeline batches per call.
Match edge endpoints through an index: In a bulk edge write, bind each endpoint with a property-map match on the indexed id (MATCH (from:Node {id: edge.from_id})) rather than a cartesianMATCH (from:Node), (to:Node) WHERE from.id = ... AND to.id = .... The cartesian form plans as a scan over the node table for every edge, which degrades badly as the graph grows; the property-map form is a primary-key index seek. Whichever form you use, and in existence checks as much as in writes, match on theidproperty Cognee stores — the string UUID the pipeline hands you — and never on a backend-internal node identifier such as Neo4j’sid(n)or an autoincrementing row number. Those identifiers are a different value (and usually a different type) from the id Cognee passes, so the comparison does not error; it silently matches nothing. Theneo4jadapter’shas_edgescomparedid(a)against the string UUID for exactly this reason and reported every edge as absent, which made the cognify dedup step below write a fresh copy of every edge on each re-cognify.
Let existence checks fail loudly: Inhas_edges— and in any read or existence-check method whose empty result is meaningful — do not catch a store error and return an empty list. An empty return must mean “the backend answered, and nothing matched”; it must never mean “the query failed”.has_edgesis the batch check the cognify dedup uses to decide which edges are new, so a swallowed failure tells it that none of the edges exist, it writes all of them, those writes fail against the same broken store, and the run finishes reporting success with nothing persisted. Log the error and re-raise, as theneo4jadapter does, and reserve[]for the genuine empty-input short-circuit. A successful check has a shape contract too: return the subset of the input triples that exist, as(source_id, target_id, relationship_name)strings. Not booleans, and not a list aligned one-to-one with the input — the caller,find_existing_edge_identities, unpacks each returned item into three values and treats the result as the set of edges to skip, so a per-input boolean list is not a compatible substitute.
Carry edge properties out ofget_connections:get_connections(node_id)returns(source_node, edge, target_node)triples for every edge into or out of the node, and the middle element must carry the edge’s stored properties alongside itsrelationship_name. Consumers read those properties by key, and a missing key does not raise — it produces a quietly wrong answer. Cognee’s document-deletion path derives theEdgeTypevector-row id of a chunk’scontainsedges fromedge["edge_text"]; when that key is absent it falls back to the relationship name, computes a different id, and leaves the real vector rows behind. Watch for driver helpers that flatten a relationship and discard its properties on the way out: Neo4j’sresult.data()reduces a relationship to(start_props, type, end_props), so the adapter has to requestproperties(relation)explicitly and merge it in.
2. Test with a Dedicated Script
Your contribution should have an example showcasing how this integration should be configured and used.File: packages/graph/engine_name/examples/example.pyCreate a script that loads cognee and the integration package, registers it to use your new
<engine_name> provider, and runs basic usage checks (for example, remembering data, recalling it, and pruning isolated test state). For example:
3. Create a Test Workflow
File: .github/workflows/engine_name/test_engine_name.ymlCreate a GitHub Actions workflow to run your integration tests. This ensures any pull requests that modify your new engine (or the shared graph code) will be tested automatically. See an example here.
- Rename
<engine_name>appropriately. - Ensure your
pyproject.tomlhas an extras entry for any new dependencies.
5. Poetry Extras
If your new graph engine requires a special Python client or system libraries, update:pyproject.toml:
6. Final Checklist
-
Implement your
<EngineName>Adapterinpackages/<engine_name>/cognee_community_graph_adapter_<engine_name>/<engine_name>_adapter.py. -
Add a register helper (
register.py) and call it before configuring Cognee: -
Run the shared conformance suite in
packages/shared/contract_suite/graph_contract.pyagainst your adapter — it is the common contract every community graph adapter is expected to satisfy. -
Register a dataset-database handler from the same
register.pyif your backend can isolate storage per user + dataset:Without a handler, your adapter can only be used withENABLE_BACKEND_ACCESS_CONTROL=false. -
Create a test or example script
example.py. -
Create a test workflow:
.github/workflows/engine_name/test_<engine_name>.yml. -
Add required dependencies to
pyproject.tomlextras. - Open a PR to verify that your new integration passes CI.