Technical Documentation: topoteretes/cognee
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Overview (README.md)
๐ Read the research paper: Optimizing the Interface Between Knowledge Graphs and LLMs for Complex Reasoning โ Markovic et al., 2025
When to use Cognee
- Build a Company Brain. Bring documentation, conversations, tickets, code, and agent work into shared memory. Help your team and agents connect a decision to the discussion and implementation behind it. Explore Company Brain.
- Give agents memory across runs. Retain project context, past decisions, fixes, and learned rules. Distill useful session lessons into durable knowledge that another session can retrieve. Connect your agent.
- Ground agents in your domain. Structure memory around the entities and relationships your application needs, with custom data models and ontologies. Explore ontologies.
Choose your starting point
| I want toโฆ | Start here |
|---|---|
| Build memory without an LLM | Local Python quickstart |
| Explore a prebuilt graph without downloading models | Bundled demo |
| Generate answers with a local or hosted LLM | Optional LLM setup |
| Give an existing agent memory | Plugins and MCP |
| Run Cognee on my infrastructure | Deployment options |
| Use a managed service | Cognee Cloud |
Quickstart
Requires Python 3.10โ3.14.
1. Install Cognee with pip, uv, or your preferred Python package manager. The gliner extra brings the local extraction model used when no LLM key is configured:
uv pip install "cognee[gliner]"Run locally without an LLM
2. Build and query memory. With no LLM key configured, Cognee extracts the graph with the local GLiNER model and embeds with a local embedding model; both download on first use.
Save this as quickstart.py and run python quickstart.py if you are feeling old school, or tell your LLM to do it:
import asyncio
import cognee
async def main():
# Extract a knowledge graph and embed the text with local models.
await cognee.remember(
"Marie Curie was born in Warsaw and worked at the University of Paris.",
dataset_name="local_quickstart",
)
# Retrieve the matching source text; no LLM generates an answer.
results = await cognee.recall(
"Where was Marie Curie born?",
datasets=["local_quickstart"],
)
for result in results:
print(result)
if __name__ == "__main__":
asyncio.run(main())The same workflow is available from the CLI:
cognee-cli remember "Marie Curie was born in Warsaw." -d local_quickstart
cognee-cli recall "Where was Marie Curie born?" -d local_quickstartText ingestion, retrieval, and session storage work without an LLM. LLM-dependent improvement stages skip automatically.
Generated answers and media processing that requires a vision or transcription model need additional LLM configuration.
The bundled GLiNER extractor is a demo of Cognee's small-model pipeline. For a production-ready version with higher accuracy and broader label coverage, reach out to us.
Optional: Configure the LLM
3. Add an LLM to get generated answers instead of retrieved passages:
import os
os.environ["LLM_API_KEY"] = "YOUR OPENAI_API_KEY"Alternatively, create a .env file using our template.
Once a key is set, Cognee uses OpenAI for language models and embeddings, and processing and generated answers make provider calls. See installation, other providers, or local Ollama models for other setups.
Explore the bundled demo
To explore a prebuilt graph without downloading extraction or embedding models:
cognee-cli demoThis command works with the base pip install cognee package. It loads bundled sample data and runs keyword search without an API key. Use the local quickstart above to build a graph from your own text.
How Cognee works
Cognee builds connected memory from different sources. Text becomes entities, relationships, and searchable chunks; code becomes a graph of symbols and dependencies. Session distillation curates accepted lessons into permanent memory.
At query time, retrieval selects relevant graph, vector, or code context. Your application can inspect the retrieved evidence and use it to answer a question or continue an agent task.
| Operation | What it does | Learn more |
|---|---|---|
remember |
Store content or code in permanent memory, or in a session when a session ID is supplied. | Store memory |
recall |
Retrieve context and answers, using automatic routing or a chosen search strategy. | Query memory |
improve |
Enrich memory, apply feedback, and bridge session knowledge into the graph. | Improve memory |
forget |
Remove a specific item or dataset. | Delete memory |
Explore the architecture and session lifecycle.
Connect your agent
Install the Claude Code plugin:
claude plugin marketplace add topoteretes/cognee-integrations
claude plugin install cognee-memory@cogneeOr install the Codex plugin. Enable hooks first, either with the CLI:
codex features enable hooksor in ~/.codex/config.toml:
[features]
hooks = trueThen add the marketplace and the plugin:
codex plugin marketplace add topoteretes/cognee-integrations --ref main
codex plugin add cognee@cogneeFollow the plugin setup guide to configure local or remote memory.
| Interface | Start here |
|---|---|
| Claude Code memory plugin | Install and configure the plugin |
| OpenClaw memory plugin | Install @cognee/cognee-openclaw |
| Cursor, Cline, and other MCP clients | Cognee MCP guide and server README |
| Python applications | Python API reference |
| TypeScript applications | TypeScript SDK |
| Rust applications | Cognee-RS |
| Applications using HTTP | REST API reference |
Browse the integrations repository for agent frameworks, plugins, and source connectors. Each guide describes its setup and memory capture behavior.
To inspect a local installation in the UI:
cognee-cli -uiThe UI launcher requires Node.js/npm; Docker is needed for its MCP service. See local UI setup.
Explore examples
- Build a small Company Brain from text, code, and session lessons.
- Build a Company Brain from your database, ticket export, and docs (or a sample company), then browse it in the UI and query it from Claude Code or Codex.
- Import memory from Mem0, Letta, Zep, or Graphiti using the COGX exchange format.
- Run with local Ollama models, including a local embedding model.
- Visualize your knowledge graph and inspect its connections.
- Browse runnable examples for ingestion, sessions, feedback, and custom pipelines.
- Run the prebuilt API with Docker Compose or use the deployment templates.
- Explore community adapters and add-ons.
Deploy Cognee
For a local API demo using a prebuilt image, follow the minimal Docker Compose guide. It includes a persistent-volume configuration and explains the single-user demo settings. Or run the image directly:
docker run --rm -it -p 8000:8000 \
-e LLM_API_KEY="sk-..." \
-e ENABLE_BACKEND_ACCESS_CONTROL=false \
-v cognee_storage:/cognee-storage \
cognee/cognee:mainENABLE_BACKEND_ACCESS_CONTROL=false is the single-user/local posture โ without it the API defaults to multi-tenant mode and every /api/v1 call requires an authenticated user. To keep authentication on instead, set DEFAULT_USER_PASSWORD to make the default account loginable (see the compose guide). Note that --rm discards container-local data on exit; the -v cognee_storage:/cognee-storage named volume keeps your memory across runs.
To run the API, UI, and MCP server from a source checkout, clone this repository, enter its directory, copy .env.template to .env, and configure your providers. Then run:
docker compose --profile ui --profile mcp upThe default ports are API 8000, UI 3000, and MCP 8001. For deployment beyond a local demo, configure authentication, persistent storage, and compatible backends using the permissions guide and deployment templates. Cognee Cloud provides the managed option.
The cognee/cognee image ships with the GLiNER runtime baked in, so text ingestion and retrieval work in Docker without an LLM key; the local extraction and embedding models download on first use. Set LLM_API_KEY (as above) when you want generated answers.
Run the Whole Memory Layer on Postgres
Graph memory traditionally means operating a stack โ a graph database for relationships, a vector database for embeddings, Redis for sessions, and a relational database for metadata โ all deployed, secured, and paid for before an agent remembers anything. Since cognee 1.0 you can run the entire memory layer on a single Postgres instance.
โ ๏ธ Warning: Using Postgres as a graph store is currently released as a demo feature. The production-ready version is available as a licensed product. Use the demo to keep relational metadata, PGVector, and graph state working together in one Postgres service.
Benchmarks and research
The BEAM evaluation measures conversational memory using synthetic long-context conversations and an LLM judge. The reported runs use Cognee's memory components with benchmark-specific data formatting, prompts, and retrieval configuration.
| BEAM context | Reported score (0โ1) | Scope |
|---|---|---|
| 100K tokens | 0.79 | Fixed hybrid retrieval; four evaluation rounds over 20 questions from one held-out conversation. |
| 10M tokens | 0.67 | Exploratory result; question-type routing selected and scored on the same question set, averaged over five rounds. |
The two settings use different conversations, ingestion models, and retrieval-selection procedures. Read the methodology, models, limitations, and reproduction instructions before comparing these scores with other systems. The report also documents the remaining reproduction gap for the distributed 10M ingestion.
For the research behind Cognee's graph/LLM interface, see Optimizing the Interface Between Knowledge Graphs and LLMs for Complex Reasoning (Markovic et al., 2025).
Latest News
- v1.6.1 โ Google Sync & Visualization (September 24, 2026): Google Drive and Gmail OAuth connectors bundled in the SDK,
/visualize/jsonstreamed in chunks for large graphs, and the GLiNER installer moved off the event loop with CPU torch installed on first use. - Document
external_metadatais stamped on every chunk and surfaced in hybrid retrieval. Breaking:dltis now a core dependency โ make sure it is installed if you manage dependencies by hand. - v1.6.0 โ Keyless workflows & pipeline reliability (September 18, 2026): build and search text memory with local models and no cloud LLM key; LLM-dependent improvement stages skip when no LLM is configured, and pipeline recovery preserves completed documents after crashes.
Community & Support
Contributing
We welcome contributions from the community! Your input helps make Cognee better for everyone. See CONTRIBUTING.md to get started.
Code of Conduct
We're committed to fostering an inclusive and respectful community. Read our Code of Conduct for guidelines.
Research & Citation
We recently published a research paper on optimizing knowledge graphs for LLM reasoning:
@misc{markovic2025optimizinginterfaceknowledgegraphs,
title={Optimizing the Interface Between Knowledge Graphs and LLMs for Complex Reasoning},
author={Vasilije Markovic and Lazar Obradovic and Laszlo Hajdu and Jovan Pavlovic},
year={2025},
eprint={2505.24478},
archivePrefix={arXiv},
primaryClass={cs.AI},
url={https://arxiv.org/abs/2505.24478},
}Docker Colima Setup (`docs/docker-colima-setup.md`)
Docker & Colima Setup for Cognee UI / MCP
The cognee-cli -ui command starts an MCP server inside a Docker container.
This requires a running Docker-compatible daemon. Both Docker Desktop and
Colima (an open-source, commercially-free alternative) are supported.
Option A: Docker Desktop
Install from https://www.docker.com/products/docker-desktop/ and start the
application. No extra configuration is needed.
Option B: Colima (macOS / Linux)
Colima provides a lightweight container
runtime without a Docker Desktop licence.
Install
# macOS (Homebrew)
brew install colima docker
# Linux (Homebrew)
brew install colima dockerStart Colima
# Basic start
colima start
# Recommended: give the VM a host-reachable network address
colima start --network-addressImportant:
--network-addressdoes not itself add ahost.docker.internal
DNS entry โ it provisions a shared-network IP that is reachable from both the
Colima VM and the host. Colima mapshost.docker.internalโhost.lima.internalby default (recent versions), so combined with a
reachable addresshost.docker.internalgenerally resolves; directly
resolving it to the--network-addressIP is still an open request
(abiosoft/colima#560). The
cognee MCP entrypoint also includes automatic fallback logic (trieshost.docker.internal, thenhost.lima.internal, then the container's
default gateway IP), so the host-API flow works even without this flag.
Verify
docker info # Should print server information without errors
docker run --rm hello-worldTroubleshooting
"Docker daemon is not responding"
| Runtime | Fix |
|---|---|
| Docker Desktop | Open the Docker Desktop application and wait for the engine to start. |
| Colima | Run colima start (or colima start --network-address). |
| Linux systemd | Run sudo systemctl start docker. |
Container cannot reach host API (`localhost` / `127.0.0.1`)
Inside a container, localhost refers to the container itself, not the host
machine. The MCP entrypoint automatically rewrites localhost / 127.0.0.1
to a reachable host address using the following fallback order:
host.docker.internal(Docker Desktop on macOS / Windows / Linux)host.lima.internal(Colima / Lima)- Default gateway IP (plain Linux Docker, typically
172.17.0.1)
If none of these work:
# Use the Docker bridge gateway directly
docker run -e API_URL=http://172.17.0.1:8000 ...
# Or use host networking (Linux only)
docker run --network host ...Colima: `host.docker.internal` does not resolve
First, give the VM a host-reachable address (most setups need only this):
colima stop
colima start --network-addressThis provisions a VM IP reachable from the host; host.docker.internal then
typically resolves via Colima's default host.docker.internal โhost.lima.internal mapping. Note that --network-address does not itself
write a DNS entry.
If the name still does not resolve (e.g. a missing or custom mapping), add it
explicitly via network.dnsHosts in ~/.colima/default/colima.yaml:
network:
dnsHosts:
host.docker.internal: host.lima.internalthen colima restart.
Minimal Docker Compose (`docs/minimal-docker-compose.md`)
Minimal docker-compose for a local try-out
Try the Cognee API server with a single copy-pasteable file โ no cloning, no
building. It uses the prebuilt cognee/cognee
image with the default local databases (SQLite, LanceDB, Ladybug), so the only
thing you need to provide is an LLM API key.
Prerequisites
- Docker with the Compose plugin (Docker Desktop, Colima, or any OCI-compatible
runtime โ see Docker & Colima Setup) - An OpenAI API key (the default LLM and embedding provider)
1. Save this as `docker-compose.yml` in an empty directory
services:
cognee:
image: cognee/cognee:main
ports:
- "8000:8000"
environment:
LLM_API_KEY: ${LLM_API_KEY:?set LLM_API_KEY to your OpenAI API key}
# Single-user try-out: no auth, shared local databases.
# Remove this line (or set it to true) for multi-tenant mode,
# which requires authentication on every API call.
ENABLE_BACKEND_ACCESS_CONTROL: "false"2. Start it
export LLM_API_KEY="sk-..." # your OpenAI API key
docker compose up3. Verify it works
curl http://localhost:8000/healthThen open http://localhost:8000/docs for the interactive API reference and
send your first requests:
# Ingest a text file
echo "Cognee turns documents into AI memory." > note.txt
curl -X POST http://localhost:8000/api/v1/add \
-F "[email protected]" \
-F "datasetName=main_dataset"
# Build the knowledge graph
curl -X POST http://localhost:8000/api/v1/cognify \
-H "Content-Type: application/json" \
-d '{"datasets": ["main_dataset"]}'
# Search it
curl -X POST http://localhost:8000/api/v1/search \
-H "Content-Type: application/json" \
-d '{"searchType": "GRAPH_COMPLETION", "query": "What does Cognee do?", "datasets": ["main_dataset"]}'Keeping data across restarts
The minimal file above stores everything inside the container, so removing the
container removes your data. To persist it, mount a named volume at the
image's built-in storage path:
services:
cognee:
image: cognee/cognee:main
ports:
- "8000:8000"
environment:
LLM_API_KEY: ${LLM_API_KEY:?set LLM_API_KEY to your OpenAI API key}
ENABLE_BACKEND_ACCESS_CONTROL: "false"
volumes:
- cognee_storage:/cognee-storage
volumes:
cognee_storage:Note:
/cognee-storageis the authoritative storage path baked into the
image (itsDockerfiledefaultsDATA_ROOT_DIRECTORYandSYSTEM_ROOT_DIRECTORYunder it, pre-created and owned by the non-rootcogneeuser, uid 1000) โ the same convention the repository'sdocker-compose.ymluses. You can relocate storage
(e.g. to/cognee-data) by overridingDATA_ROOT_DIRECTORYandSYSTEM_ROOT_DIRECTORY, but a fresh named volume mounted at a custom path is
created root-owned, so you must also make it writable for uid 1000.
Going further
- Other LLM providers (Anthropic, Gemini, Ollama, โฆ): add the matching
LLM_PROVIDER/LLM_MODEL/LLM_ENDPOINTvariables โ see.env.templatefor the full list. - UI, MCP server, Postgres, Neo4j: the repository's
docker-compose.ymlprovides these as opt-in
profiles โ see Run with Docker in the README. - Production: multi-tenant mode (
ENABLE_BACKEND_ACCESS_CONTROL=true, the
default) requires authentication and isolates data per user and dataset.
Review the security variables in.env.templatebefore
exposing the API.
Ollama Models (`docs/ollama_models.md`)
Supported Ollama Models for Structured Graph Extraction
Cognee supports using local Large Language Models (LLMs) via Ollama. However, because Cognee relies on structured output generation (using Instructor with JSON schemas) to extract knowledge graphs, the performance and reliability of the extraction pipeline depend heavily on the model's capabilities.
This guide lists recommended models, models with known limitations, and troubleshooting tips.
Model Support Matrix
1. Recommended / Validated Models
These models consistently format output correctly according to abstract JSON schemas, making them highly reliable for Cognee's graph extraction:
- Llama 3.1 (8B, 70B) (e.g.,
llama3.1:8b,llama3.1:70b) โ Highly Recommended - Llama 3.2 (3B) (e.g.,
llama3.2:3b) โ Recommended for lightweight or resource-constrained environments. - Llama 3.3 (70B) (e.g.,
llama3.3) โ Outstanding extraction capability if hardware permits. - Qwen 2.5 (14B, 32B, 72B) (e.g.,
qwen2.5:14b,qwen2.5:32b,qwen2.5:72b) โ Strong extraction and reasoning capability.
2. Known Issues & Limitations
These models have high failure rates during structured JSON schema extraction. They often output invalid JSON, verbose conversational padding, or fail to follow abstract object definitions, leading to empty or dropped graphs:
- Mistral (7B) (e.g.,
mistral,mistral:7b) โ Unstable structured JSON output, prone to schema format violations. - Phi 3 / Phi 3.5 (e.g.,
phi3,phi3.5) โ Fails to consistently adhere to Pydantic schemas. - Qwen 2.5 (7B and smaller) (e.g.,
qwen2.5:7b,qwen2.5:3b,qwen2.5:1.5b) โ Struggles with complex schemas compared to the larger $14\text{B}+$ variants. - Gemma 2 (2B, 9B) (e.g.,
gemma2:2b,gemma2:9b) โ Prone to schema validation drops.
3. Unknown / Experimental Models
Any model not listed above is treated as unvalidated/experimental. If you choose to run an unvalidated model, Cognee will emit a warning but will not block execution.
Troubleshooting Local Extraction
If you notice that cognify() is running but your final queries yield empty search results or no nodes are created, check the following:
- Verify your Model: Ensure you are using one of the recommended models (e.g.,
llama3.1:8b). - Check Temperature: Extraction runs at temperature
0.0by default on Ollama, for deterministic output formatting. If you setLLM_TEMPERATUREto something higher, lower it back to0.0in your.env. - Verify API Connection: Ensure Ollama is running and that
LLM_ENDPOINTis the bare host,http://localhost:11434. The defaultlitellm_nativeframework appends Ollama's own API paths, so the/v1form returns a 404. Usehttp://localhost:11434/v1only withSTRUCTURED_OUTPUT_FRAMEWORK="instructor". - Inspect Logging: Check the console log outputs. If Cognee catches validation errors during extraction, they will be reported as warnings.
Recall Vs Search (`docs/recall-vs-search.md`)
recall() vs search()
Both functions query the knowledge graph. recall() is the memory-API entry point and the one
to reach for by default; search() is the low-level operation it calls underneath. This page says
what recall() adds, when to drop down to search(), the two places where the same argument
means different things, and how the query router picks a strategy.
Source of truth: cognee/api/v1/recall/recall.py and cognee/api/v1/search/search.py.
What recall() adds on top of search()
- Query routing. When
query_typeis omitted andauto_route=True(the default), an
ordered table of regex rules picks aSearchTypeโ first match wins, no LLM call, so
routing is free. Withauto_route=Falseand noquery_type, recall usesHYBRID_COMPLETION. Passingquery_typebypasses the router entirely. See
The router below. - Session memory as a source.
scopeselects where results come from:"graph"(the
permanent graph viasearch()),"session"(Q&A entries in the session cache),"trace"(agent trace entries),"session_context"(the distilled guidance block), plus
opt-in"tools"(authorized external databases) and"code"(the code graph)."auto"(the
default) and"all"never imply"tools"or"code". With a baresession_idand nodatasets/query_type, a session hit short-circuits the graph search;scope="session_first"
asks for that short-circuit explicitly, with a pinned type and datasets in play. - Normalized results. Every returned entry is tagged with a
sourcekey ("graph","session","trace","session_context","tools","code","skills"), so callers
can tell where it came from, and graph entries also carry thesearch_typethat actually
ran.search()returns rawSearchResultobjects. - Skill gate. Procedural-sounding queries trigger a concurrent
SKILLSlookup whose hits
are appended taggedsource="skills"(only when exactly one dataset is targeted; disable
withSKILL_GATE_ENABLED=false). - Structured output shorthand.
response_model=validates the LLM answer against a
Pydantic model and returns it in the result'sstructuredfield.
When to call search() directly
- You need the agentic extras as first-class parameters:
skills,tools,max_iter,code_query,node_type. (recall()reaches most of these throughretriever_specific_config={...}, which you assemble yourself.) - You want raw
SearchResultobjects rather thansource-tagged entries. - You want a pinned
query_typewith no router and no session layer in the path, for
example when benchmarking one retriever. - You are writing a custom pipeline task or debugging a single retrieval stage.
Same argument, different meaning
| Argument | recall() |
search() |
|---|---|---|
session_id |
Makes the session cache a source (and may short-circuit the graph) | Only adds session history to the retrieval context; never searches the cache as a source |
omitted query_type |
Router picks one; HYBRID_COMPLETION if routing is off |
Always HYBRID_COMPLETION |
top_k |
default 15 | default 15 (the CLI's recall --top-k defaults to 10) |
only_context=True |
Same as search(); pin query_type so the hybrid retriever cannot defer to GRAPH_COMPLETION behind your back |
Returns what the LLM would have received instead of its answer: for completion types the user prompt (history, rendered question and context, session guidance) with the system prompt (the task template) alongside; retrieval-only types return their context |
Quick reference
import cognee
from cognee import SearchType
# Ordinary retrieval: let recall route the query and tag the sources.
results = await cognee.recall("What did Alice work on?", datasets=["project"])
# Session-first: answers the current conversation from the cache before touching the graph.
results = await cognee.recall("what did I just say about deadlines?", session_id="chat_1")
# Pinned strategy, no router.
results = await cognee.recall("timeline of the migration", query_type=SearchType.TEMPORAL)
# Low level: raw SearchResult objects, agentic parameters as keywords.
raw = await cognee.search(
"Which functions call UserService?",
query_type=SearchType.CODE,
code_query={"operation": "impact_analysis", "seeds": ["UserService"]},
)The router
Source: cognee/api/v1/recall/query_router.py.
Rules are checked in order and the first match wins. Shape rules โ what the input looks
like โ come first and win over intent rules, so a quoted string or a Cypher statement is
handled as what it is even when its text also reads as intent: "coding rules" is a lexical
search for that phrase, not a request for the rule list. No query in the golden table depends
on that ordering (a test enforces this). Anything unmatched goes to HYBRID_COMPLETION.
Matching is case-insensitive.
| # | Rule | Signal in the query | Routes to |
|---|---|---|---|
| 1 | quoted_phrase |
The whole query is one "quoted phrase" |
CHUNKS_LEXICAL |
| 2 | coding_rules_intent |
coding rules / coding standards / coding conventions, or code review guidelines (and the rules, standards, checklist, conventions variants) |
CODING_RULES |
| 3 | time_scoped_question |
A time preposition (in, before, after, since, between, from, on, during, as of, โฆ) directly followed by an absolute date โ or for followed by one that ends the phrase (scheduled for 2031, but not for 2000 users): a four-digit year (in 2019, in early 2024), a decade (in the 1990s), a month and year (in July 1969), a full date (on 7 November 1867, on March 1, 2024), or an ISO date (on 2024-03-01) |
TEMPORAL |
| โ | default |
Anything else | HYBRID_COMPLETION |
CYPHER is not in the table and never will be. The Cypher retriever runs
the query text verbatim through graph_engine.query(), and the whole recall
path checks read permission only โ so a routable CYPHER would let anyone who
can read a dataset destroy it by posting{"query": "MATCH (n) DETACH DELETE n"}. Reaching CYPHER requires an
explicit query_type / searchType, which is a deliberate act by the caller
rather than whatever text arrived in a request body. Pasted Cypher therefore
routes to HYBRID_COMPLETION like any other text.
The rule for what belongs in the table: auto-routing may only pick a
strategy that is at least as good as HYBRID on a default-built graph and does
not add LLM calls without an unambiguous signal.
HYBRID_COMPLETION searches document chunks, pre-computed summaries, and the
entity neighbourhood in a single LLM call. Almost every alternative strategy
sees a subset of that context, sometimes with an extra round trip attached.
So each rule above fires on an input that is not a natural-language question at
all โ pasted Cypher, a fully quoted literal, a request for the rule list โ and
for which HYBRID is the wrong operation rather than a worse one. A rule that
fires on an ordinary question is a regression even when its target sounds
better suited.
That is why these are not auto-routed, even though they are validquery_type values:
GRAPH_COMPLETION_COTruns up to four reasoning iterations. "Why" and
"explain" questions are answered by the default strategy; pinquery_type=SearchType.GRAPH_COMPLETION_COTwhen you want the loop.GRAPH_COMPLETION_CONTEXT_EXTENSIONadds traversal rounds. HYBRID already
includes entity neighbourhoods, so "related to" questions stay on the
default.GRAPH_SUMMARY_COMPLETIONdoes not read pre-computed summaries: it runsGRAPH_COMPLETIONand then makes a second LLM call to summarize the answer.
Routing "summarize the report" there would drop HYBRID's document and
summary lanes and add a round trip.TEMPORALis routed, but only on an absolute date behind a time
preposition. The default pipeline extractsTimestampnodes, and the
retriever reranks HYBRID's own candidates by the window โ it never sees less
context than HYBRID โ so the only cost of the route is the one LLM call that
reads the window out of the question. That call is wasted when there is no
window to find, which is why a barewhen,since monday,the 2019 report,ticket 2048orQ4 2024stay on the default. Known limitation: a
four-digit quantity after a preposition (in 2048 bytes) reads as a year;
the retriever then finds no window and answers as HYBRID. With no LLM
configured, recall picksCHUNKSbefore the router runs, so a temporal
question never reaches the interval extraction.- "Exact"/"verbatim" phrasing does not select
CHUNKS_LEXICAL. BM25 tokenizes
the raw query, so the trigger word itself becomes a rare, high-IDF search
term and skews the ranking it was meant to sharpen.quoted_phrasehas no
such problem: its trigger is punctuation, which tokenization drops. - Incidental code tokens (
def,import,class Foo(,.py,refactor,lint) do not selectCODING_RULES. That retriever reads only thecoding_agent_rulesnodeset and returns nothing on an ordinary graph.
When a routed strategy comes up empty
A routed type is a guess, so recall() never lets one do worse than the
default. When the router picked a type other than the default, the query is
retried once as HYBRID_COMPLETION in two cases:
- the backend rejects the type; or
- the search returns nothing and the empty result means the lane was
unavailable โCHUNKS_LEXICALwith no lexical hits,CODING_RULESon a
dataset with no rules nodeset.
The search history records the type that actually answered. Two things are
never second-guessed: a type you pinned yourself returns empty or raises as
before, and a failure of the default itself is raised rather than hidden โ
there is nothing left to fall back to, so the error is real.
Bypassing the router
| Surface | Route automatically | Pin a strategy |
|---|---|---|
SDK recall() |
omit query_type (default) |
pass query_type=SearchType.X; auto_route=False forces HYBRID_COMPLETION without routing |
REST POST /api/v1/recall |
omit searchType or pass null (default) |
pass a value |
CLI cognee-cli recall |
omit --query-type |
--query-type X (choices in cognee/cli/config.py:SEARCH_TYPE_CHOICES) |
All three surfaces auto-route by default โ with one exception. When no usable
LLM key is configured, recall picks CHUNKS before the router is consulted,
because nothing can write a completion answer; the router never runs and an
explicit query_type is the only way to select a strategy. That branch keys
off LLM availability, not off the extractor that built the graph.
On every surface, omitting the type also makes the session a search source
whenever a session_id is given: alone it short-circuits the graph on a hit,
alongside datasets both contribute. Pinning a type leaves the graph as the only
source unless you ask for the session by name with scope. REST clients that relied on the oldHYBRID_COMPLETION default should pass "searchType": "HYBRID_COMPLETION"
explicitly.
Seeing what ran
Graph results carry the resolved type as search_type. The CLI prints it in
the Found N result(s) using ... line. The recall span carries the type ascognee.search.type and, when the router chose it, the rule name ascognee.recall.route_rule โ so which rule fires on real traffic is answerable
without reproducing the query. The router also logs the rule name at DEBUG
level, and never logs the query text.
Adding a rule
Add a (name, compiled pattern, SearchType) tuple to _RULES inquery_router.py, then add cases to the golden table and the negative
invariants in cognee/tests/unit/api/v1/recall/test_query_router.py. Two
structural tests constrain what you can add: the new target must be inROUTABLE_TYPES, and the new pattern must not change how any query in the
golden table routes. Keep the size principle above in mind: a rule that sends
ordinary questions to a slower or narrower retriever is a regression, not an
improvement.
Related: the search-type list in CLAUDE.md ("SEARCH: Retrieval"), examples/guides/recall_core.py,examples/guides/hybrid_retrieval_recall.py.
Slack History (`docs/slack-history.md`)
Import Slack conversations into Cognee
The SDK's Slack integration can import selected channel history or complete
threads into an existing Cognee dataset. It uses native add, update,cognify, and datasets.delete_data; the connector stores only selection and
run status alongside the existing encrypted Slack connection.
Install or upgrade the Slack app
- Configure the existing Cognee Slack OAuth connection. Add these bot scopes:
channels:history,groups:history, andgroups:read, alongside the existingcommands,chat:write,im:write, andchannels:readscopes. Reinstall or
reconnect the app to grant the new scopes. Reading history is opt-in; granting
scopes does not start an import. - Invite the app to each selected channel. The Slack person who connected the
workspace must also be a member. Bulk imports are managed by the connection
owner; linking another member's Cognee account does not grant them the
installation's ability to export channel history. - Add
/cognee-importwith the same request URL as/cognee-remember:https://YOUR_API/api/v1/slack/commands. - Add a message shortcut named Remember entire thread, callback ID
remember_thread. Set the Interactivity request URL and the Options Load
URL tohttps://YOUR_API/api/v1/slack/interactive. The options URL supplies
destination datasets from the caller's actual Cognee permissions. - Restart the updated API. Scheduled refreshes run inside the API process once
a selection is enabled.SLACK_HISTORY_SYNC_ENABLED=falsedisables this
worker for deployments that use an external scheduler instead.
The existing /cognee-remember command and single-message shortcut keep their
behavior. /cognee-ask already searches imported memory using Cognee's dataset
permissions. No frontend or MCP changes are required.
Select information in Slack
Run /cognee-import (seven days by default), or /cognee-import 14. The dialog
lets you choose a destination dataset, channels, thread links, and ongoing sync.
The message shortcut starts the same dialog with that entire thread selected.
The dataset picker offers datasets where the owner has read, write, and
delete permission: native document replacement and source deletion require
those rights. The importer rechecks permissions before fetching and before
writing. A channel allowlist also applies to the selected source channels,
not only the channel where a command is invoked.
By default, dates select threads started during the period. The optional
active during the period mode scans older roots too, so a recent reply to an
old thread is included. This requires more Slack calls. In both modes the
selected conversation includes all currently available replies, even those
outside the date range. Thread links select complete conversations independently
of channel dates. Files/attachments and DMs are not imported; message text,
authors, timestamps and source links are retained. Cognee bot replies are skipped
to avoid feeding its answers back into its own source memory.
Confirmation is private and sent after indexing completes. Large imports may
outlive Slack's response URL or a server restart; use the status endpoint below
and retry an interrupted selection. Repeating an import does not duplicate its
documents.
Dataset and node-set organization
Use datasets as access boundaries and node sets to organize channels. Each
conversation receives slack, slack:<team_id>, andslack:channel:<channel_id> node sets. Node sets do not enforce permissions.
Choose an appropriately restricted destination for private channels; source
membership is not copied into Cognee ACLs and this feature never grants access.
The importer manages only documents carrying its own source stamp in the target
dataset. Manually remembered Slack notes, other sources, and other datasets are
not swept by source reconciliation.
API and SDK
All HTTP endpoints require a Cognee bearer token/session belonging to the Slack
connection owner. JSON fields use snake_case.
POST /api/v1/slack/history/{team_id}/import
PUT /api/v1/slack/history/{team_id}/sync
GET /api/v1/slack/history/{team_id}Example import request:
{
"dataset_id": "YOUR_DATASET_UUID",
"channel_ids": ["C0123456789"],
"days": 14,
"thread_mode": "active",
"thread_links": []
}Instead of days, use timezone-aware oldest and optional latest dates, for
example 2026-09-01T00:00:00Z. For selected conversations alone, omitchannel_ids and dates and supply thread_links, or structuredthreads: [{"channel_id": "C0123456789", "ts": "1700000000.000001"}].
To configure periodic refresh, PUT:
{
"enabled": true,
"interval_seconds": 21600,
"selection": {
"dataset_id": "YOUR_DATASET_UUID",
"channel_ids": ["C0123456789"],
"days": 14
}
}A saved selection resolves days into a fixed start, then extends to the
present on each refresh. There is one selection per workspace and destination
dataset; saving it again replaces that selection. Separate destination datasets
can have different source selections. To disable one, PUT the same selection
with enabled: false. Disabling stops future runs, not an already running import.
GET returns saved selections and the last run's status/counts, never tokens or
message content. Failed refreshes become eligible for retry after ten minutes;
successful refreshes use the configured interval (six hours by default).
For an embedded SDK application, the same entry points are:
from cognee.modules.integrations.slack.history import import_slack_history
from cognee.modules.integrations.slack.history_models import SlackHistoryRequest
result = await import_slack_history(
team_id,
SlackHistoryRequest(dataset_id=dataset_id, channel_ids=channel_ids, days=14),
user=owner,
)Use the HTTP API when Cognee already runs in another process. The runnable
HTTP example avoids opening
the server's embedded databases in a second process.
Sync guarantees and limits
- Every channel/thread page is fetched before native writes begin. API errors,
missing pagination cursors, and request/message caps fail the import rather
than marking a partial snapshot complete. Defaults are 1,000 requests and
50,000 messages; API callers can explicitly raise these bounded limits. - Stable identity includes destination dataset, Slack workspace, channel, and
root timestamp. Changed threads go through nativeupdate, so stale derived
graph/vector content is removed. Identical imports reuse the same documents. - Apps using rotating Slack tokens refresh expiring credentials before history
reads. Rotation preserves source selections, channel restrictions and account
ownership; it cannot reactivate a concurrently revoked connection. - Scheduled reconciliation also re-fetches previously imported conversations
in the selected channels, catching edits, deleted replies, late replies, and
changes made while the API was offline. A confirmed missing thread deletes
only its corresponding source document through native deletion. Lost scopes,
channel access, and transient errors never stand in for deletion. - A rolling date window is not retention. A shorter import never deletes
older documents just because they are absent from that window. Reconciliation
reflects what Slack currently makes available; it cannot recover history
already removed by Slack retention policies. - Native multi-document updates are retryable, not atomic across the complete
import. If indexing fails after ingestion, retrying still runs indexing even
when raw content is unchanged. One-off imports interrupted by process exit
require a retry; enabled scheduled selections recover on a later tick. - Run one API worker with local embedded stores. Native dataset locks and this
refresh worker are process-local; a multi-worker deployment must designate a
single history worker and serialize external import requests.
Slack API references: history,
threads,
rate limits. Internal customer
apps retain the normal history tier; stricter limits apply to some commercially
distributed apps. The connector honors Retry-After and bounded retries.
Supply Chain Provenance (`docs/supply_chain_provenance.md`)
Supply-chain provenance & release attestations
Cognee's release pipeline produces verifiable provenance for every shipped
artifact so consumers can independently confirm that a package on PyPI or an
image on Docker Hub was built from this repository by our CI โ not tampered with
in transit or rebuilt by a third party.
This covers three layers of evidence:
| Artifact | Mechanism | Where it is recorded |
|---|---|---|
| PyPI sdist + wheel | PEP 740 digital attestations via PyPI Trusted Publishing | PyPI project page ("Provenance" / "Attestations") |
| PyPI sdist + wheel | SLSA build provenance (actions/attest-build-provenance) |
GitHub repo โ Attestations tab |
| Docker images | in-toto provenance + SBOM (buildx provenance/sbom) |
Pushed alongside the image manifest |
The relevant workflows are .github/workflows/release.yml (tagged library releases),.github/workflows/dev_canary_release.yml (weekly dev canaries), and.github/workflows/release_mcp.yml (manual MCP releases).
One-time setup: PyPI Trusted Publishing
PyPI only accepts and displays PEP 740 attestations when a package is uploaded
through Trusted Publishing (OpenID Connect), not an API token. The library
workflows have already been switched to OIDC (id-token: write, noUV_PUBLISH_TOKEN), but a project owner must register the trusted publishers
on PyPI once. release_mcp.yml is the exception until its publisher exists:
see cognee-mcp still uploads with a token.
PyPI scopes trusted publishers per project. Register three publishers, one
per workflow: two on cognee and one on the separate cognee-mcp project.
Open the publishing settings for cognee
and cognee-mcp.Under Add a new publisher โ GitHub, add each publisher to the
PyPI project listed below:Field Release publisher Canary publisher MCP release publisher PyPI project cogneecogneecognee-mcpOwner topoteretestopoteretestopoteretesRepository cogneecogneecogneeWorkflow name release.ymldev_canary_release.ymlrelease_mcp.ymlEnvironment (leave blank) (leave blank) (leave blank)
The Environment value must match the
environment:declared on the
publishing job. The workflows do not set one, so leave this blank โ if you
later add a GitHub Actions environment, set the same name on both sides or the
OIDC publish step fails auth.
Optional hardening (not configured): a GitHub Actions environment with
required reviewers / branch restrictions can gate publishing so commit access
alone does not grant PyPI publishing rights. To enable it, addenvironment: <name>to the publishing job and set the matching name on the
PyPI publisher above. Note that required reviewers on the canary workflow
would block its weekly cron.
- Save all three publishers.
Release MCP by running release_mcp.yml from the main branch in the Actions
tab. Other refs fail explicitly. The workflow reads the version fromcognee-mcp/pyproject.toml, refuses to run if that version is already on PyPI,
uploads it, and tags the commit cognee-mcp-v<version> (its own namespace,
since cognee-mcp versions independently of the library).
After the publishers are registered, the next library release uploads with
provenance automatically.
โ ๏ธ Do not run a library release before its publishers are registered โ the
publish step will fail OIDC auth. The release workflow isworkflow_dispatch-only, so you control the timing.
cognee-mcp still uploads with a token
The cognee-mcp publisher in the table above has not been registered, and only
the owner of that PyPI project can add it. Until then release_mcp.yml uploads
with the PYPI_TOKEN repository secret, the account-wide token the library used
before it moved to OIDC. Two consequences:
cognee-mcpfiles on PyPI carry no PEP 740 attestations. That includes 0.5.6,
which was uploaded by hand with the same token on 2026-10-01 after the first
OIDC run failed withinvalid-publisher. The SLSA build provenance on GitHub
is still produced for every workflow release.- The token can publish every project its account owns,
cogneeincluded, so
it is a broader credential than this workflow needs.
Moving over needs no workflow change, because the publish step uses Trusted
Publishing whenever the secret is absent. Do it in this order:
- Register the
cognee-mcppublisher from the table above. - Delete the
PYPI_TOKENsecret.
The order matters. Deleting the secret first leaves MCP releases with no way to
authenticate.
Verifying provenance as a consumer
PyPI package (PEP 740)
pip surfaces attestations from the PyPI "Provenance" section on the project /
file pages. You can also fetch the integrity/provenance metadata via the PyPI
JSON API:
curl -s https://pypi.org/pypi/cognee/json | jq '.urls[].provenance'PyPI package (SLSA, GitHub-hosted)
Download a wheel/sdist and verify the GitHub-hosted build provenance with the
GitHub CLI:
gh attestation verify ./cognee-<version>-py3-none-any.whl --repo topoteretes/cogneeA successful verification confirms the artifact's SHA-256 digest was produced by
a workflow in topoteretes/cognee.
Docker image (in-toto provenance + SBOM)
# Provenance attestation
docker buildx imagetools inspect cognee/cognee:latest \
--format '{{ json .Provenance }}'
# SBOM attestation
docker buildx imagetools inspect cognee/cognee:latest \
--format '{{ json .SBOM }}'How this maps to trust signals
- Package provenance (HVTracker / supply-chain trackers): flips from None
to present once Trusted Publishing uploads PEP 740 attestations. - OpenSSF Scorecard
Signed-Releasesโ satisfied by attested PyPI artifacts.Token-Permissionsโ release workflows declare a minimal top-levelpermissions: contents: readand opt intoid-token/attestationsonly
where needed.Pinned-Dependenciesโ all actions in the release workflows are pinned to
full commit SHAs (with a version comment).
When bumping a pinned action, update both the SHA and its trailing# vX.Y.Z comment together.
Turso Local (`docs/turso-local.md`)
Running cognee on Turso (local)
Turso is a rewrite of SQLite in Rust that keeps the SQLite file format and SQL dialect and adds
multi-version concurrency control (MVCC) and vector distance functions. cognee can run all four
of its stores on it, as local database files:
| Layer | Provider setting | What it stores | Adapter |
|---|---|---|---|
| Relational | DB_PROVIDER=turso |
users, datasets, pipeline runs, ACLs | cognee/infrastructure/databases/relational/sqlalchemy/TursoAdapter.py |
| Graph | GRAPH_DATABASE_PROVIDER=turso |
graph-as-tables (graph_node, graph_edge) |
cognee/infrastructure/databases/graph/turso/adapter.py |
| Vector | VECTOR_DB_PROVIDER=turso |
one table per collection, F32_BLOB embeddings |
cognee/infrastructure/databases/vector/turso/TursoVectorAdapter.py |
| Session cache | CACHE_BACKEND=turso |
session Q&A, traces, context, usage logs (cache.db) |
cognee/infrastructure/databases/cache/sql/SqlCacheAdapter.py |
The four layers are independent: any one of them can be Turso while the others stay on their defaults.
The session cache mirrors the sqlite backend exactly (a cache.db next to the relational
database, same tables and upserts) and only swaps the driver; it is not switched automatically
with DB_PROVIDER, so set CACHE_BACKEND=turso explicitly, as with postgres.
All three share cognee/infrastructure/databases/turso/: the SQLAlchemy dialect
(sqlite+cognee_turso://), the TURSO_* settings, the transaction/retry policy and the
file-cleanup rule.
Versions
| Component | Version | Notes |
|---|---|---|
pyturso (import name turso) |
>=0.7.2,<0.8 (pinned in the turso extra) |
the Turso rewrite engine and its Python bindings; SELECT turso_version() proves it is the executing engine |
| SQL dialect tracked by the engine | SQLite 3.50.4 | SELECT sqlite_version() |
| SQLAlchemy | 2.0.x (cognee's pin) | the bundled sqlite+aioturso dialect fails on 2.0.4x+, see "Upstream findings" |
| Python | 3.10 โ 3.14 | wheels for Linux x86_64/aarch64, macOS, Windows |
pyturso 0.8.0 release candidates exist. They add recursive CTEs, but 0.8.0rc11 aborts the
process on the neighborhood query cognee used, so cognee stays on 0.7.x and avoids recursive CTEs.
Setup
pip install cognee"[turso]" # or: uv pip install -e ".[turso]"
# .env โ pick any subset
DB_PROVIDER=turso
GRAPH_DATABASE_PROVIDER=turso
VECTOR_DB_PROVIDER=turso
CACHE_BACKEND=turso
LLM_API_KEY=... # cognify needs an LLM as usualFiles land under the system databases directory (SYSTEM_ROOT_DIRECTORY/databases): the
relational database as DB_NAME, and with access control on (the default) one graph file
(graph_<dataset_id>.db) and one vector file (<dataset_id>.turso.db) per dataset. Paths can be
pinned with GRAPH_DATABASE_URL / VECTOR_DB_URL (absolute paths). Remote Turso databases are
not supported in this version: a set DB_TURSO_URL, GRAPH_DATABASE_KEY or a libsql:// /https:// vector URL is a hard error, never a silent fallback to a local file.
Engine-wide settings (TursoConfig, env prefix TURSO_):
| Setting | Default | Meaning |
|---|---|---|
TURSO_JOURNAL_MODE |
wal |
wal: SQLite-compatible write-ahead log, one writer at a time. mvcc: Turso's concurrent writes for the graph and vector stores (below); the relational DB and session cache always stay on wal. |
TURSO_BUSY_TIMEOUT_MS |
120000 |
how long a writer waits for the write lock in wal mode before database is locked |
TURSO_CONFLICT_RETRIES |
5 |
how many times the graph and vector write paths re-run a transaction that hit a write conflict |
Runnable example
uv run python examples/guides/turso_local_example.py ingest # add -> cognify -> search
uv run python examples/guides/turso_local_example.py verify # fresh process: search the stored files
uv run python examples/guides/turso_local_example.py cleanup # forget(everything=True) + remove filesThe script sets the three providers itself and keeps its files under .turso_example/ next to it.verify runs in a new process and answers from the files ingest wrote, which is the persistence
check. TURSO_JOURNAL_MODE=mvcc runs the same flow with the graph and vector stores on concurrent
writes.
Migrating from the previous `turso` provider
Before this version the turso provider ran on plain SQLite (aiosqlite) for the relational and
graph layers and on libsql-experimental for vectors. Existing local files need no migration: a
WAL-mode file written by stock SQLite opens on the Turso engine and vice versa (verified both
ways). The remote replica mode of the old vector and relational adapters is gone; configure a local
file or another provider. Setting DB_TURSO_URL / DB_TURSO_AUTH_TOKEN is now a hard error, and theturso extra no longer installs libsql-experimental.
A Turso file can also be open in only one process at a time. The old aiosqlite-based relational
and graph providers allowed several processes on one file, so a deployment that ran the API server
next to a separate SDK/CLI process, or several server workers, on the same files breaks on upgrade.
Transactions and concurrent writes
wal mode is SQLite's behaviour: writers take one lock, others wait up to TURSO_BUSY_TIMEOUT_MS
and then fail with database is locked. Files stay readable by stock SQLite. The driver's implicitBEGIN only starts at the first INSERT/UPDATE/DELETE, so the graph and vector adapters open their
write transactions with BEGIN IMMEDIATE themselves (write_transaction()): a read-modify-write
(tag merges and removals, payload updates) holds the write lock from its first read, and a
concurrent writer waits and then reads its commit instead of being overwritten from a stale read.
mvcc mode (PRAGMA journal_mode=mvcc, applied on every connection) makes the graph and
vector adapters open write transactions with BEGIN CONCURRENT. Independent connections commit
in parallel; only two transactions that write the same row conflict, and the engine reports
that eagerly, at the statement, as Write-write conflict, aborting the later transaction. Every
graph and vector write is one self-contained transaction, which the adapters re-run
(retry_on_conflict, jittered backoff, TURSO_CONFLICT_RETRIES attempts).
The relational DB and the session cache ignore mvcc and always run in wal
(TursoConfig.wal_only()). Their writes go through ORM sessions whose callers run arbitrary code
between statements, so a conflicted transaction cannot be replayed: under mvcc the second of two
writers to one row (pipeline status, dataset rows, the session-context upsert) would fail and its
write would be lost. Under wal it waits on busy_timeout and succeeds. The price is write
throughput under heavy concurrency: in a local benchmark of concurrent session-cache writes in one
process, wal fell from about 1,200 to about 150 writes/s at 64 writers, while mvcc held about
1,200 on distinct rows but, without retries, lost most same-row writes.
Constraints of the mode:
- DDL needs a plain
BEGIN("DDL statements require an exclusive transaction"); cognee runs schema
creation and migrations insideexclusive_transaction(), which switches the statement. - The database file gains a
-logcompanion and is no longer readable by stocksqlite3
(cognee's dataset cleanup removes-wal,-shmand-log). Switching a file back towal
restores SQLite compatibility. Migrationc3d5e7f9a1b2, which heals a standalonecache.db,
opens a Turso cache through pyturso, so it also handles a cache file left inmvccmode. - Plain
BEGINwriters still serialize withdatabase is locked, so mixing tools that do not useBEGIN CONCURRENTgains nothing. - MVCC is experimental upstream. The default stays
wal.
Measured in cognee/tests/e2e/turso/test_concurrent_writes.py (both modes): 6 writers on
independent connections, 40 rows each, through the graph adapter, the vector adapter and raw
threads; a forced same-row conflict; retry recovery; and a reopen after the writes. The relational
adapter's tests pin that two same-row writers both commit with TURSO_JOURNAL_MODE=mvcc set.
Test commands
# Adapter-level, no LLM needed (also the CI job "Turso Tests")
pytest cognee/tests/unit/infrastructure/databases/turso_backend \
cognee/tests/unit/infrastructure/databases/relational/test_turso_adapter.py \
cognee/tests/unit/infrastructure/databases/relational/test_create_relational_engine.py \
cognee/tests/unit/infrastructure/databases/graph/test_turso_graph_dataset_database_handler.py \
cognee/tests/unit/infrastructure/databases/vector/test_turso_adapter.py \
cognee/tests/unit/infrastructure/databases/cache/test_sql_cache_turso.py \
cognee/tests/unit/infrastructure/databases/cache/test_sql_adapter_crud.py \
cognee/tests/e2e/turso/test_turso_adapter.py \
cognee/tests/e2e/turso/test_concurrent_writes.py
# The same adapter tests on concurrent writes (CI runs both modes)
TURSO_JOURNAL_MODE=mvcc pytest <the paths above>
# Full search suite with every layer on Turso (needs LLM + embedding keys)
DB_PROVIDER=turso GRAPH_DATABASE_PROVIDER=turso VECTOR_DB_PROVIDER=turso CACHE_BACKEND=turso \
pytest cognee/tests/test_search_db.py -vCoverage: the adapter tests run in both wal and mvcc in CI. The end-to-end search suites run
in wal only, so mvcc is not covered end to end.
Every Turso test asserts SELECT turso_version() (or the cognee_turso driver name), so a test
that silently ran on SQLite would fail.
Known limitations
| Limitation | Effect on cognee | Handling |
|---|---|---|
No approximate vector index (libsql_vector_idx / vector_top_k are not supported) |
similarity search is an exact vector_distance_cos scan |
~16โ20 ms for top-15 over 20k ร 384-dim rows on a laptop; fine for local datasets, not for millions of rows |
| No recursive CTEs (0.7.x) | k-hop neighborhoods, connected components | one query per hop; union-find in Python |
No scalar subquery in ON CONFLICT DO UPDATE SET |
vector upsert that merged belongs_to_set in SQL |
tags are read and merged in Python, then a plain excluded.payload upsert |
json_each().value returns a string element's raw JSON text, escapes included (B\u00fccher for Bรผcher); payloads are written with json.dumps, which escapes non-ASCII characters, quotes and backslashes |
node set names in vector search filters and tag removal | names are compared on the decoded element (payload ->> je.fullkey); tag removal filters the names in Python inside one write transaction |
Bind parameters must be None, numbers, str or bytes (no register_adapter) |
raw text() statements binding datetime/UUID |
SQLAlchemy-typed columns are unaffected; raw statements use a typed bindparam |
Quoted identifiers are stored lowercased in sqlite_master |
has_collection by exact name, PascalCase collection detection |
case-insensitive lookup; collections detected by schema (id, payload, vector) |
Parenthesized joins in a FROM clause (JOIN (a JOIN b ON โฆ)) are rejected |
SQLAlchemy emits them for joined-table inheritance (User/Tenant are Principal subclasses) |
the dialect's compiler (turso/compiler.py) flattens the tree into a left-deep join chain |
A join chain that combines several inner joins, a constant filter and a LEFT JOIN can drop a join condition and return extra rows; an affected query returns the wrong rows on every run |
none known: no current cognee query has the shape, but users and permissions are read through joins on this engine | avoid the shape in new SQL, or check its rows against stock SQLite; test_turso_engine_bugs.py reproduces the smallest case and starts failing once a pyturso release fixes it |
VACUUM requires an experimental engine flag |
none (cognee never runs VACUUM) |
โ |
| MVCC files are unreadable by stock SQLite | external tooling on a live mvcc database |
switch the file to wal first |
| One synchronous connection used from two threads at once aborts the process (Rust panic) | the vector adapter shares one connection | every call runs under _connection_lock; keep it that way |
A database file can be open in one process at a time, in both journal modes. The engine holds the file lock until that process exits (closing its connections does not release it), and it also blocks stock sqlite3 |
the API server and a separate SDK/CLI process, or several server workers, cannot share one Turso deployment; the stock SQLite backends allow this. cognee raises TursoDatabaseInUseError, naming the file, instead of the engine's Locking error |
run one cognee process per set of Turso files (the server's default gunicorn -w 1 is fine), or give each process its own SYSTEM_ROOT_DIRECTORY |
Upstream findings
Each finding will be filed upstream with a minimal reproduction and linked from SDK-664. When a
finding is fixed in a released pyturso, the matching workaround (noted in the table above) can
be retired.
sqlite+aiotursodialect fails on SQLAlchemy 2.0.4x+:'AsyncAdapt_turso_dbapi' object has no attribute 'has_stop'. cognee's dialect sets it.- The dialect's reflection mixin returns
[]for indexes, foreign keys and unique constraints
althoughPRAGMA index_list/foreign_key_listwork; with it, cognee's idempotent migrations
collide on existing indexes and Alembic batch operations would drop constraints. cognee's
dialect restores stock SQLite reflection. Fixed upstream in the 0.8.0 release candidates. - Scalar subquery in
ON CONFLICT DO UPDATE SET:Parse error: Subquery is not supported in this position(0.7.2 and 0.8.0rc11). - An outer
json_each()over an unaliased column is misresolved insideUPDATE/DELETE: on
0.7.2 the tag-removalUPDATE ... json_each(payload, ...)runs without error but changes no rows
(stock SQLite strips the tag), and qualifying the column with a quoted mixed-case table name fails
withno such table(finding 6). Aliasing the table (AS t,json_each(t.payload, ...)) works.
Earlier shapes failed withbind index 1 is out of bounds; 0.8.0rc11 reports'json_each' is not a function. WITH RECURSIVE:Recursive CTEs are not yet supported(0.7.2); 0.8.0rc11 accepts the syntax
but aborts the process (Fatal Python error: Abort) on cognee's neighborhood query.- Quoted identifiers lowercased in
sqlite_master(stock SQLite preserves case). - Only primitive bind parameter types; no
register_adapterequivalent. JOIN (a JOIN b ON โฆ):Parenthesized FROM clause subqueries are not supported, although
the engine reports SQLite 3.50 (SQLite has accepted the form since 3.7.16).json_each().valuereturns a string element's raw JSON text instead of the decoded string
(0.7.2): for the stored array["B\u00fccher"](howjson.dumpswrites["Bรผcher"]) it
returns the escape textB\u00fccheritself, where stock SQLite returnsBรผcher.payload ->> je.fullkeyandjson_extract(payload, je.fullkey)decode correctly.- A join condition is dropped (0.7.2): with
u=(2,1),a=(1,1),p=(3,1)andbempty
(id INTEGER PRIMARY KEY, k INTEGER),SELECT u.id, a.id FROM u JOIN a ON u.id = a.id AND a.k = 1 JOIN p ON p.k = a.k LEFT JOIN b ON u.k = b.kreturns(2, 1); stock SQLite returns
no rows. Removing the constant filter, the third inner table or theLEFT JOINgives the
right result; moving the conditions toWHEREdoes not.
Contributing Add Catalog Entry (`docs/contributing/add-catalog-entry.md`)
Adding a catalog entry
The Cognee Integrations Hub and Use-Case Gallery are generated from the YAML entries under catalog/entries/. Adding a new integration, use-case, or community package is a single-PR change: catalog entry, plus a runnable example the entry points at.
This guide covers the mechanics. The contract itself is catalog/schema.json.
What the catalog is for
Users looking to adopt cognee ask two questions:
- "Does cognee work with my stack?" (answered by the Integrations Hub)
- "How do I do the thing I need done?" (answered by the Use-Case Gallery)
Every catalog entry is one card in one of those views. It carries just enough context for a user to decide it's the right starting point, plus a runnable example they can copy-paste.
Anatomy of an entry
Every entry is a YAML file under catalog/entries/{integrations,use-cases,packages}/. The filename stem must match the entry's id. Example:
# catalog/entries/use-cases/document-qa.yaml
id: document-qa
title: Document Q&A backed by a knowledge graph
kind: use-case
stack: use-case
tags:
- document-qa
- graph-rag
- recall
- reading-comprehension
summary: Answer questions grounded in a set of documents using cognee's graph-completion retrieval instead of plain RAG.
what_youll_build: A recall pipeline that ingests documents, extracts entities and relationships into a graph, and answers questions with citations back to source chunks.
quickstart: |
git clone https://github.com/topoteretes/cognee.git
cd cognee
uv pip install cognee
export LLM_API_KEY=your_openai_key
python examples/guides/recall_core.py
expected_output: |
A ranked list of retrieved passages plus a synthesized natural-language
answer citing the original documents. Compare against a plain RAG baseline
to see graph-completion's contribution.
difficulty: easy
example_path: examples/guides/recall_core.pyFields
The full schema lives in catalog/schema.json. Highlights:
id: Lowercase, dashes, no spaces. Must match the filename stem. This is the stable machine identifier every downstream tool uses.title: The human-readable name as it appears on the Hub card.kind:integration,use-case, orpackage. Drives which subdirectory the file lives under and which view it appears in.stack: The primary technology bucket (llm-provider,vector-store,graph-store,relational-store,framework,agent-runtime,workflow-tool,observability,loader, oruse-case). Drives the Hub filter chips.tags: Free-form filter labels. Redundant withstackon purpose so users can search by outcome or provider without hitting a taxonomy wall.summary: One sentence, shown on the Hub card. Answers "does this work with my stack".what_youll_build: One sentence promising a concrete outcome. Answers "what do I get if I follow the quickstart".quickstart: A copy-paste block that gets a newcomer running. Include install, env, and a single run command. Multi-line YAML block scalar (quickstart: |).expected_output: A concrete description of what the user should see after running the quickstart. Doesn't need to be exact values, but should be specific enough that a user knows if their run worked.difficulty:easy,medium, oradvanced. Measures the effort for a new user, not code complexity.repo,path: Required for integrations and packages. Point at the source in one oftopoteretes/cognee,topoteretes/cognee-community, ortopoteretes/cognee-integrations.example_path: Required for use-cases. Path withintopoteretes/cogneeto a runnable script.inventory_slug: Optional. If your integration already has a slug incognee-integrations/integrations/inventory.yml, set it here so the drift check keeps them aligned.docs_url: Optional link to a longer doc page (e.g.docs.cognee.ai/integrations/openai).
Adding an entry: the workflow
- Pick the right subdirectory (
integrations/,use-cases/, orpackages/). - Copy an existing entry as a starting point:bash
cp catalog/entries/use-cases/document-qa.yaml catalog/entries/use-cases/your-new-entry.yaml - Edit every field. The schema catches typos in field names, so a stray letter turns into a CI error, not a silent bug.
- Make sure the
example_path(for use-cases) or thepath(for integrations/packages pointing attopoteretes/cognee) resolves against the current checkout. - If you're referencing an example that doesn't have a mocked test yet, coordinate with #3601 so the example runs in CI.
- Run the validation locally (a standard
uv syncalready provides thepyyamlandjsonschemathe tooling needs):The loader must exit 0 for the entry to be considered valid โ that is the blocking gate. The drift check is advisory: it tolerates coverage gaps (integrations in the inventory but not yet in the catalog) and flags stale references, and it runs as a non-blocking CI step so an upstream change never fails your PR.bashuv sync uv run python -m catalog.loader uv run python -m catalog.inventory_sync - Open a PR. The
Catalogworkflow will re-run both checks.
Coverage gaps
If you add an entry with an inventory_slug, the drift check confirms the slug exists upstream. If you don't add one, you're implicitly saying "this is a brand-new integration not in inventory.yml yet." That's fine.
If you want to help close a coverage gap, run the drift check locally to see the outstanding list, then add entries one at a time. Batch PRs of 5-10 entries are welcome; larger batches are harder to review.
What if I'm adding an integration that lives in a private fork?
Point repo at the private fork, or leave repo and path unset and treat it as a use-case (kind: use-case) with a public example_path. The catalog is designed for public discovery, so anything a user can't reach shouldn't be an entry.
How To Guides Cognee Sdk Deployment Coolify (`docs/how-to-guides/cognee-sdk/deployment/coolify.mdx`)
title: "Deploy Cognee on Coolify" icon: "anchor"
Coolify is an open-source, self-hostable PaaS โ a Heroku/Netlify
alternative that runs on your own server and deploys anything Docker can run. Because
Cognee ships a docker-compose.yml, Coolify can build, run, route, and TLS-terminate the
Cognee API for you, so you go from "it works on my laptop" to a public HTTPS endpoint without
writing any infrastructure code.
This guide deploys the Cognee API server (the cognee service). It uses a slim,
Coolify-friendly compose so the deployment is reliable on a modest server, and explains how to
add the optional services (databases, MCP server, UI) when you need them.
Architecture
flowchart LR
client(["API client / browser"]) -->|HTTPS| traefik
subgraph server["Your server โ managed by Coolify"]
traefik["Coolify proxy (Traefik)<br/>Let's Encrypt SSL"]
cognee["cognee API<br/>:8000 ยท /health"]
traefik --> cognee
cognee -.->|"file-based, default"| vol[("SQLite ยท LanceDB ยท Ladybug<br/>persisted on a named volume")]
cognee -.->|"optional"| ext[("Postgres ยท Neo4j ยท Redis")]
endBy default Cognee uses file-based databases (SQLite + LanceDB + Ladybug, Cognee's embedded
graph engine), so a working deployment needs no external database โ just one API key.
Prerequisites
- A server (VPS or bare metal) running a 64-bit Linux distro (Debian/Ubuntu recommended).
Coolify itself idles at ~0.6โ1 GB RAM, so plan for 4 GB+ even though the documented
minimum is 2 GB. - A running Coolify instance (v4). If you don't have one yet, follow Step 1 below first.
- An LLM API key. Cognee defaults to OpenAI, so an OpenAI key works out of the box
(LLM_API_KEY); any supported provider can be configured later. - (Optional) a domain name with a DNS
Arecord pointing at your server, for a clean
HTTPS URL. Coolify can also hand out a freesslip.iohostname.
Deployment at a glance
flowchart TD
A["Install Coolify on a Linux server"] --> B["Create New Resource โ Public Repository"]
B --> C["Build Pack: Docker Compose"]
C --> D["Set environment variables<br/>(LLM_API_KEY, ENV=prod, โฆ)"]
D --> E["Assign a domain for HTTPS<br/>(SERVICE_FQDN_COGNEE_8000)"]
E --> F["Deploy"]
F --> G{"/health returns 200?"}
G -->|Yes| H["Live: Cognee API over HTTPS"]
G -->|No| I["Check Logs โ Troubleshooting"]
I --> FStep 1 โ Install Coolify (skip if you already have it)
On a fresh server, run the official one-line installer as root (or with sudo):
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bashOpen the dashboard at http://<your-server-ip>:8000 and register the first account โ the
first user to register owns the instance, so do this immediately. Ports used by Coolify:
22 (SSH), 80 / 443 (proxy + SSL), and 8000 / 6001 / 6002 (dashboard, realtime,
terminal). Lock 8000 down to your own IP once you're set up.
Step 2 โ Add Cognee as a Docker Compose resource
- Open (or create) a Project and pick an environment (e.g. production).
- Click Create New Resource and choose Public Repository.
- Paste the repository URL (the upstream repo or your fork):text
https://github.com/topoteretes/cognee - Coolify defaults the build pack to Nixpacks โ open that dropdown and select
Docker Compose. - Set Branch to
main, Base Directory to/, and Docker Compose Location to the
compose file you want to deploy (see below).
Use a slim, Coolify-friendly compose
Cognee's root docker-compose.yml is tuned for local development and has three rough edges
on Coolify. Knowing them saves hours:
Repo docker-compose.yml |
Why it bites on Coolify |
|---|---|
Builds the image from source (build:) |
The multi-stage uv build is heavy and can OOM a small server. Coolify's own docs recommend a prebuilt image on low-RAM hosts. |
Declares a custom network (cognee-network) |
Coolify warns that custom networks cause intermittent 504s; it auto-creates an isolated network per stack. |
Gates optional services behind profiles: |
Coolify does not reliably honor compose profiles (issue #6395) โ it may start every service regardless. |
The fix is a small, production-oriented compose that uses the official prebuilt image
(cognee/cognee:main), declares no custom network, has a single service (no profiles), injects
config via Coolify environment variables, and persists data on a named volume. (It also drops the
repo compose's published 5678 debugger port.) Commit this file to your fork and point
Docker Compose Location at it, or paste it via Coolify's raw compose editor:
services:
cognee:
image: cognee/cognee:main
restart: always
ports:
- "8000:8000"
environment:
- LLM_API_KEY=${LLM_API_KEY}
# Canonical env var (ENVIRONMENT is a deprecated alias). Use exactly "prod":
# any other value enables FastAPI debug; "dev"/"local" also add Gunicorn auto-reload.
- ENV=prod
- DEBUG=false
- LOG_LEVEL=INFO
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}
# Keep the file-based databases on the persistent volume below.
- DATA_ROOT_DIRECTORY=/app/.cognee/data
- SYSTEM_ROOT_DIRECTORY=/app/.cognee/system
volumes:
- cognee_data:/app/.cognee
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
volumes:
cognee_data:Reference: services in the full repo compose
If you do deploy the upstream docker-compose.yml, this is what it contains. Only services
without a profiles: key start under plain Docker Compose โ the cognee API (8000) andredisinsight (5540):
| Service | Profile | Default ports | Purpose |
|---|---|---|---|
cognee |
(none) | 8000 | Core API server |
redisinsight |
(none) | 5540 | Redis GUI |
cognee-mcp |
mcp |
8001 | MCP server for IDE integrations |
frontend |
ui |
3000 | Experimental web UI |
postgres |
postgres |
5432 | PostgreSQL + pgvector |
neo4j |
neo4j |
7474 / 7687 | Neo4j graph database |
redis |
redis |
6379 | Redis cache |
Ports are host mappings; cognee-mcp listens on 8000 inside its container (mapped to host 8001).
Step 3 โ Configure environment variables
Open the resource's Environment Variables tab. Add variables one-by-one in Normal View,
or switch to Developer View to paste a block of KEY=VALUE lines at once. Cognee reads
variables straight from the container environment (they take precedence over any .env file),
so whatever you set here is applied on the next deploy.
The only variable you must set is the API key:
| Variable | Required | Default | Notes |
|---|---|---|---|
LLM_API_KEY |
Yes | โ | OpenAI key by default; also used for embeddings if no separate key. |
LLM_MODEL |
No | openai/gpt-5-mini |
Override the model. |
LLM_PROVIDER |
No | openai |
openai, anthropic, gemini, ollama, โฆ |
CORS_ALLOWED_ORIGINS |
No | * |
Lock down to your domain(s) in production. |
Step 4 โ Persist data and choose your databases
File-based (default, zero setup). Cognee defaults to SQLite (relational), LanceDB (vector)
and Ladybug (graph) โ Ladybug is Cognee's embedded graph engine; Kuzu is also supported. To
survive redeploys, keep them on a named volume โ Coolify persists
named volumes automatically (it appends the resource UUID to the name). The slim compose does
this with cognee_data plus DATA_ROOT_DIRECTORY / SYSTEM_ROOT_DIRECTORY.
External databases (optional). To move onto Postgres or Neo4j, set the matching provider
variables โ pointing only DB_HOST at a server is not enough. Run the database as a separate
Coolify resource, or add its service directly to your compose (declared normally, since Coolify
won't gate it behind a profile):
# PostgreSQL (relational) โ the bundled image is pgvector/pgvector:pg17
DB_PROVIDER=postgres
DB_HOST=postgres # the compose service name, on the same Coolify network
DB_PORT=5432
DB_USERNAME=cognee
DB_PASSWORD=cognee
DB_NAME=cognee_db
# Vector store โ built-in options are lancedb (default) and pgvector
VECTOR_DB_PROVIDER=pgvector
# Graph store โ ladybug (default), kuzu, neo4j, โฆ
GRAPH_DATABASE_PROVIDER=neo4j
GRAPH_DATABASE_URL=bolt://neo4j:7687
GRAPH_DATABASE_USERNAME=neo4j
GRAPH_DATABASE_PASSWORD=pleaseletmeinStep 5 โ Expose the API: domain, port & SSL
The cognee service listens on port 8000. To publish it through Coolify's proxy with
automatic HTTPS, assign a domain to the service โ either in its Domains field, or with
Coolify's magic variable in the compose:
SERVICE_FQDN_COGNEE_8000=https://cognee.example.comThe identifier is the service name (COGNEE) with the container port appended (_8000).
Entering an https:// domain makes Coolify request and auto-renew a Let's Encrypt
certificate via Traefik. (Magic variables in a Git-sourced compose require Coolify
v4.0.0-beta.411+; on older builds, assign the domain in the service's Domains field instead.)
Step 6 โ Deploy and verify
Click Deploy and open the Logs. A healthy start runs the database migrations and then
Gunicorn binds the server:
Debug mode: false
Environment: prod
HTTP port: 8000
Bind address: 0.0.0.0
Running database migrations...
Database migrations done.
Starting server...With ENV=prod, Gunicorn logs at error level, so the startup banner is quiet โ rely on the
health check below to confirm readiness.
Check the health endpoint โ a ready instance returns HTTP 200:
curl https://cognee.example.com/health{ "status": "ready", "health": "healthy", "version": "x.y.z" }If a critical database or storage check fails, /health returns HTTP 503 with{"status": "not ready", "health": "unhealthy", โฆ} โ there is no separate "starting" state, and a
degraded-but-usable instance still returns 200. For a component-by-component breakdown callGET /health/detailed, and the interactive API docs live at /docs.
Production hardening
Before exposing Cognee publicly, review these defaults:
- Authentication.
ENABLE_BACKEND_ACCESS_CONTROL=True(default) requires auth on the API
โ aREQUIRE_AUTHENTICATION=Falseis ignored while access control is on. Create a user, or for
a single-user setup behind a token setENABLE_BACKEND_ACCESS_CONTROL=False. - JWT secret. Set
FASTAPI_USERS_JWT_SECRETto a securely generated secret, identical across
replicas. When unset, each server process generates its own secret at startup, so tokens
stop working after a restart and are not accepted by other replicas. - CORS. Replace the default
*inCORS_ALLOWED_ORIGINSwith your real front-end origin(s). - Secrets. Never commit API keys โ set them only as Coolify environment variables.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Build is slow / server runs out of memory | Building the image from source compiles many extras | Deploy the prebuilt image (cognee/cognee:main) via the slim compose (Step 2) |
Intermittent 504 Gateway Timeout |
A custom networks: block in the compose |
Remove custom networks; let Coolify manage the network (the slim compose has none) |
| Unwanted services (Postgres, Neo4jโฆ) start anyway | Coolify doesn't honor compose profiles: (#6395) |
Deploy a compose that contains only the services you want (the slim compose) |
ENV / ENVIRONMENT changes have no effect |
Repo compose hard-codes ENV=local inline |
Edit the service environment:, or use the slim compose |
| Data disappears after a redeploy | File-based DBs weren't on a persistent volume | Use a named volume + DATA_ROOT_DIRECTORY / SYSTEM_ROOT_DIRECTORY (Step 2) |
/health never turns ready |
Can't reach a configured external DB, or migrations failed | Check Logs; verify DB_* / GRAPH_* values and that the DB is reachable |
401/403 on API calls (not /health) |
Access control is on by default | Authenticate, or set ENABLE_BACKEND_ACCESS_CONTROL=False for single-user mode |
| Let's Encrypt certificate fails | DNS not pointing at server, ports 80/443 closed, or Cloudflare proxied | Fix DNS/ports; set Cloudflare DNS to "DNS only" |
Cost estimate
Cognee runs comfortably on a small VPS; the main variable cost is LLM API usage.
| Tier | Specs | Approx. server cost |
|---|---|---|
| Minimal (file-based DBs, light use) | 2 vCPU / 4 GB | ~$5โ12 / month |
Recommended (headroom for cognify) |
4 vCPU / 8 GB | ~$15โ30 / month |
| External DBs / heavier graphs | 8 vCPU / 16 GB | ~$30โ60 / month |
LLM token costs are separate and depend on your provider, model, and how much data you ingest.
Next steps
- Cognee SDK & API reference
- Build your graph:
addโcognifyโsearch - Connect Cognee to your IDE by also deploying the MCP server (
cognee/cognee-mcp)
--- METRICS ---
- Files Extracted: 10
- Estimated Token Budget: ~23528 tokens
- Canonical Reference: https://codewiki.google/github.com/topoteretes/cognee