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.

role-based multi-agent orchestration framework
★ 40kPythoncrewAIInc/crewAI

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.