Skip to content

Unified Memory

Verified against crewai==1.14.3a2 (source: crewai/memory/unified_memory.py, crewai/memory/memory_scope.py, crewai/memory/storage/*.py).

CrewAI’s memory system centres on a single class — Memory — that works either attached to a crew or on its own. Older docs mention “short-term”, “long-term”, and “entity” memory; in 1.14 those collapse into scoped views over the unified store.

from crewai import Memory
mem = Memory() # lancedb under ./memory by default
mem.remember(
"Ada prefers Postgres over MongoDB for transactional workloads.",
scope="/users/ada",
categories=["preferences", "db"],
importance=0.8,
)
matches = mem.recall("what database does ada like?", limit=3)
for m in matches:
print(f"{m.score:.2f} {m.record.content}")

The default configuration picks up OPENAI_API_KEY for both analysis (gpt-4o-mini) and embedding (text-embedding-3-small). Both are pluggable — see below.

FieldTypeDefaultNotes
llmstr | BaseLLM"gpt-4o-mini"Used for query analysis and consolidation (deep-recall flow only).
storageStorageBackend | str"lancedb""lancedb", "qdrant-edge", a path string (becomes a LanceDB path), or a custom backend.
embeddercallable | dict | NoneNoneEmbedder callable, provider-config dict (e.g. {"provider": "google", "config": {...}}), or None for default OpenAI.
recency_weightfloat0.3Composite score weighting. Must sum with the other two weights to ~1.0.
semantic_weightfloat0.5
importance_weightfloat0.2
recency_half_life_daysint30Recency score halves every N days.
consolidation_thresholdfloat0.85Similarity above which the LLM tries to merge near-duplicates on save.
consolidation_limitint5Max candidates compared during consolidation.
default_importancefloat0.5Used when importance=None and the LLM can’t infer one.
confidence_threshold_highfloat0.8Deep-recall early-exit threshold.
confidence_threshold_lowfloat0.5Below this, deep-recall spawns another round.
exploration_budgetint1Max LLM-driven deep-recall rounds.
read_onlyboolFalseIf True, remember() is a no-op and returns None.
root_scopestr | NoneNoneAll operations are implicitly nested under this path.
BackendValueInstallNotes
LanceDB (default)"lancedb"includedFile-backed; fast for up to ~100k records.
LanceDB at a path"./some/dir"includedAny string without the special markers becomes a LanceDB path.
Qdrant Edge"qdrant-edge"pip install qdrant-clientIn-process Qdrant with payload indexing, better filtering.
CustomMyBackend()Any class implementing the StorageBackend protocol from crewai.memory.storage.backend.
from crewai import Memory
from crewai.memory.storage.lancedb_storage import LanceDBStorage
mem = Memory(storage=LanceDBStorage(path="./prod-memory"))
record = mem.remember(
"Ada prefers Postgres.",
scope="/users/ada", # optional; LLM infers if None
categories=["preferences"], # optional; LLM infers
metadata={"source": "slack"},
importance=0.8, # 0-1; LLM infers if None
source="slack-msg-1234", # provenance; used for private-record filtering
private=False,
root_scope=None, # per-call override of instance-level root_scope
)
  • Returns the saved MemoryRecord.
  • Synchronous; the save goes through the single-worker thread pool.
  • Triggers consolidation: if a very similar record exists the LLM may merge them.
mem.remember_many([
"Ada is a backend engineer.",
"Ada works remote from Toronto.",
], scope="/users/ada", categories=["bio"])
  • Fires-and-forgets — returns an empty list immediately. The save runs in the background.
  • The next recall() waits for pending saves (read barrier).
matches = mem.recall(
"tell me about ada",
scope="/users/ada", # optional prefix filter
categories=["bio"], # optional filter
limit=10,
depth="deep", # or "shallow"
source="slack-msg-1234", # only if you store private records
include_private=False,
)
  • depth="shallow" — single embed + vector search. Fast, no LLM calls.
  • depth="deep" (default) — the LLM rewrites the query into sub-queries, selects scopes, and iterates using exploration_budget.
  • Results are ranked by a composite score = semantic_weight * similarity + recency_weight * recency + importance_weight * importance.
# Delete everything older than a cutoff for a user
from datetime import datetime, timedelta
deleted = mem.forget(
scope="/users/ada",
older_than=datetime.utcnow() - timedelta(days=365),
)
# Edit one record
mem.update(record_id, content="Ada now prefers DuckDB for analytics.", importance=0.9)

Most apps want a bounded view rather than the whole memory:

# Scope: everything under /projects/phoenix
phoenix = mem.scope("/projects/phoenix")
phoenix.remember("Design doc frozen on 2026-04-01.")
# Slice: read-only view across multiple scopes
shared = mem.slice(
scopes=["/users/ada", "/teams/platform"],
categories=["decisions"],
read_only=True,
)
matches = shared.recall("who owns auth?")
  • scope(path) is a two-way view — can read and write under path.
  • slice(scopes=[...]) is read-only by default and can span many paths.
from crewai import Crew, Memory
mem = Memory(root_scope="/crew/research")
crew = Crew(
agents=[a, b],
tasks=[t1, t2],
memory=mem, # or memory=True for a fresh Memory() with defaults
)
  • memory=True — CrewAI spins up a default Memory(); fine for exploration but ties you to OPENAI_API_KEY.
  • memory=mem — full control; set root_scope to avoid cross-crew leakage.
  • memory=mem.scope("/some/path") or memory=mem.slice([...]) — scoped view.

During kickoff the agent saves observations with remember_many and recalls with recall(..., depth="deep"). The crew calls drain_writes() before returning so every save has finished persisting.

shared = Memory(storage="lancedb")
ada_view = shared.scope("/users/ada")
ada_crew = Crew(agents=[...], tasks=[...], memory=ada_view)

Each user gets an isolated prefix without needing a separate DB.

prod = Memory(storage=LanceDBStorage(path="/mnt/memory/prod"))
dev = Memory(storage=LanceDBStorage(path="./dev-memory"))
for rec in prod.list_records(limit=5000):
dev.remember(rec.content, scope=rec.scope, categories=rec.categories,
metadata=rec.metadata, importance=rec.importance)
from crewai.tools.memory_tools import memory_tools_for
tools = memory_tools_for(memory=mem)
agent = Agent(role="Analyst", goal="...", backstory="...", tools=tools)

crewai.tools.memory_tools wraps remember / recall as agent-callable tools.

prod_view = Memory(storage=prod_storage, read_only=True)

Safer to pass to untrusted agents; writes silently succeed as no-ops so you can still attach to a crew.

from crewai.rag.embeddings.types import EmbedderConfig
mem = Memory(embedder={"provider": "google", "config": {"model": "text-embedding-004"}})

Any provider config that build_embedder accepts works here.

  • depth="deep" costs LLM calls. For fast one-off lookups, pass depth="shallow" — it skips query analysis entirely.
  • remember_many is async. If you need the records back, call remember in a loop or call drain_writes() first.
  • Three weights should sum to ~1.0. They’re not normalised automatically; wildly off numbers make the ranking useless.
  • Default embedder needs OpenAI. Memory() with no args tries to reach OpenAI for both the LLM and embedder. Pass your own to avoid the dependency.
  • Private records only show up in recall when source= matches the record’s source, unless include_private=True.
  • Two processes writing the same LanceDB path will corrupt it. Use Qdrant Edge or a remote store if you need multi-process writes.
  • entity memory is not a separate class. The legacy EntityMemory was folded into scoped views — use categories=["entity:<name>"] or distinct scopes.