CrewAI's memory looks solved out of the box. You flip memory=True, the crew starts "remembering," the demo works, and you ship. Then a real user comes back the next day and the crew greets them like a stranger — because the memory that made the demo feel smart was short-term and entity memory, backed by a local store scoped to that one process. When the run ended, it evaporated. Nothing was user-scoped, nothing crossed sessions, nothing was shared between the two machines behind your load balancer.
This is the walkthrough for fixing that with Mem0 — the managed path in three lines, the self-hosted path with its one sharp edge, and how to run durable and throwaway memory in the same crew.
What Mem0 actually replaces#
CrewAI splits memory into four things: short-term (this run's working context), entity (facts about people/things it's seen), long-term (task outcomes, in a local SQLite file), and contextual (the orchestration layer that assembles the rest into a prompt). Wiring in Mem0 doesn't bolt a second brain onto all four — it replaces short-term and entity memory with an external, user-scoped provider. Long-term and contextual stay native.
That division is the whole point. The volatile, per-conversation state that used to die with the process now lives in Mem0, keyed by a user_id, and reloads the next time that user shows up. And Mem0 doesn't dump raw transcripts into storage — it runs LLM extraction over each interaction and keeps the salient facts, so recall stays cheap instead of replaying the whole history.
Path 1: Mem0 Cloud — the three-line version#
Install, set the key, and hand the crew a memory_config. That's the entire change.
pip install crewai mem0ai
export MEM0_API_KEY="m0-your-key"
export OPENAI_API_KEY="sk-..." # crew LLM + Mem0's extraction
from crewai import Crew, Agent, Task, Process
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
process=Process.sequential,
memory=True, # turn the memory system on
memory_config={
"provider": "mem0",
"config": {"user_id": "founder_rosa"}, # who this memory belongs to
},
)
# First session — the crew learns things about founder_rosa
crew.kickoff(inputs={"topic": "our Q3 pricing experiment"})
The user_id is the load-bearing field. Run the same crew tomorrow with the same user_id and Mem0 reloads what it extracted — the pricing experiment, the preferences, the entities — before the agents start. Change the user_id and you get a clean, isolated store for a different person. That single key is the difference between one shared memory blob and true per-user recall.
The user_id is not a label. It's the isolation boundary. Everything Mem0 remembers is scoped to it — which means a bug that reuses one user_id across customers is a data-leak, not a typo.
You can scope tighter with agent_id (memory for one agent, not the whole crew) or run_id (memory for one execution). Most teams start with user_id alone and add the others when a specific agent needs its own persistent notes.
Path 2: Mem0 OSS — same shape, one trap#
If you can't send interaction data to a managed service — compliance, residency, or you just already run a vector DB — Mem0 self-hosts. The crew wiring is nearly identical, but the configuration goes somewhere different, and this is where people lose an afternoon.
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
memory=True,
memory_config={
"provider": "mem0",
"config": {"user_id": "founder_rosa"},
# NOTE: local engine config goes under local_mem0_config, NOT config.
"local_mem0_config": {
"vector_store": {
"provider": "qdrant",
"config": {"host": "localhost", "port": 6333},
},
"llm": {"provider": "openai", "config": {"model": "gpt-4o-mini"}},
"embedder": {"provider": "openai",
"config": {"model": "text-embedding-3-small"}},
},
},
)
The trap: put your vector-store/LLM/embedder settings under config and CrewAI silently ignores them — it reads local engine settings from local_mem0_config. No error, no warning; it just quietly falls back to defaults, and you discover it when nothing lands in your Qdrant. Keep user_id in config, keep the engine in local_mem0_config. (This is CrewAI's 1.14 pluggable-backends philosophy one layer down: you own the store.)
Durable and throwaway memory: ExternalMemory#
Sometimes you want two kinds of memory at once — durable facts about the user that should live forever, plus scratch memory for just this run that shouldn't pollute the long-term store. That's what the ExternalMemory object is for, with an explicit run_id.
from crewai.memory.external.external_memory import ExternalMemory
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, write_task],
memory=True,
external_memory=ExternalMemory(
embedder_config={
"provider": "mem0",
"config": {"user_id": "founder_rosa", "run_id": "campaign_2026_q3"},
}
),
)
Keep run_id stable across a multi-step campaign and those agents share a scratchpad; drop it and everything falls back to durable user_id scope. It's the clean way to avoid one run's working notes leaking into the next.
Where this sits#
Adding Mem0 is CrewAI working exactly as its 1.14 pluggable-backends release intended: the framework orchestrates, a specialist owns the store. If you're still deciding which specialist, the mem0 vs Zep vs Letta comparison is the next stop — but if you're already on CrewAI and just need it to stop forgetting your users, the change is memory_config and a user_id. Ten minutes, and the crew finally remembers who it's talking to.



