Memory: remember and recall across runs
Memory is a store that keeps short facts with a vector for each, so recall can return the ones closest to a question and scopes can keep one customer's facts apart from another's.
Last updated: 28 Sep, 2026 · CrewAI 1.15
Knowledge, from the last lesson, holds fixed rules you load up front. Memory holds what the desk learns as it works: a customer's preference, an order it looked up. It needs an embedder to turn text into numbers, and a model for the analysis it does when you leave details out.
An embedder you can read
Any function that takes a list of texts and returns a list of vectors can be the embedder. Without one, Memory calls a hosted service that needs a key.
import hashlib, math
def word_hash(texts):
vectors = []
for text in texts:
vector = [0.0] * 64
for word in text.lower().split():
word = word.strip(".,?!")
vector[int(hashlib.md5(word.encode()).hexdigest(), 16) % 64] += 1.0
length = math.sqrt(sum(x * x for x in vector)) or 1.0
vectors.append([x / length for x in vector])
return vectorsEach word adds 1 to one of 64 slots picked by its hash, and the list is scaled to length 1. Texts that share words point the same way. A hosted embedder knows that "email" and "contacted" are related; this one only matches words.
View the code here
import hashlib
import math
def word_hash(texts):
vectors = []
for text in texts:
vector = [0.0] * 64
for word in text.lower().split():
word = word.strip(".,?!")
vector[int(hashlib.md5(word.encode()).hexdigest(), 16) % 64] += 1.0
length = math.sqrt(sum(x * x for x in vector)) or 1.0
vectors.append([x / length for x in vector])
return vectors
from crewai.tools import tool
ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}
@tool
def lookup_order(order_id: str) -> str:
"""Look up an order's shipping status by its id, such as A17."""
status = ORDERS.get(order_id)
return f"{order_id} {status}." if status else f"{order_id} is not an order we have."
import json
import os
import re
from crewai import BaseLLM
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"
class ShopLLM(BaseLLM):
script: list = []
def supports_function_calling(self):
return True
def call(self, messages, tools=None, **kwargs):
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
if self.script:
return self.script.pop(0)
return self.decide(messages, tools or [])
def decide(self, messages, tools):
last = messages[-1]
if last["role"] == "tool":
return last["content"]
text = last["content"]
orders = re.findall(r"\b[A-Z]\d+\b", text)
want_refund = "refund" in text.lower()
chosen = None
for t in tools:
fn = t["function"]
label = (fn["name"] + " " + (fn.get("description") or "")).lower()
is_refund = "refund" in label
if want_refund and is_refund:
chosen = fn["name"]
break
if not want_refund and not is_refund and ("look up" in label or "status" in label):
chosen = fn["name"]
break
if orders and chosen:
args = json.dumps({"order_id": orders[0]})
return [{"id": f"call_{orders[0]}", "type": "function",
"function": {"name": chosen, "arguments": args}}]
if "working with:" in text:
context = text.split("working with:")[1].strip().split("\n\n")[0]
return f"Dear customer, {context}"
if orders:
return f"I have no way to look up {orders[0]} yet."
return "Hello. Which order is this about?"
Building the store
from crewai import Memory
from embed import word_hash
from shop_llm import ShopLLM
memory = Memory(llm=ShopLLM(model="shop"), embedder=word_hash)The records are stored with LanceDB on disk, so they survive between runs.
Remembering three facts
from crewai.events import crewai_event_bus
with crewai_event_bus.scoped_handlers():
memory.remember("Asha prefers email, not phone calls.",
scope="/customer/asha", categories=["preference"], importance=0.8)
memory.remember("Asha's order A17 shipped on 3 March.",
scope="/customer/asha", categories=["order"], importance=0.5)
memory.remember("Ravi's order C40 is waiting for stock.",
scope="/customer/ravi", categories=["order"], importance=0.5)Each fact gets a scope, a path like a folder, plus categories and an importance. With all three supplied, no model call is needed; leave them out and Memory asks its model to choose them. scoped_handlers sets CrewAI's own listeners aside so this lesson's output stays readable.
Recalling the closest fact
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
from crewai import Memory
from crewai.events import crewai_event_bus
from embed import word_hash
from shop_llm import ShopLLM
memory = Memory(llm=ShopLLM(model="shop"), embedder=word_hash)
memory.reset() # start from an empty store so the demo is repeatable
with crewai_event_bus.scoped_handlers():
memory.remember("Asha prefers email, not phone calls.",
scope="/customer/asha", categories=["preference"], importance=0.8)
memory.remember("Asha's order A17 shipped on 3 March.",
scope="/customer/asha", categories=["order"], importance=0.5)
memory.remember("Ravi's order C40 is waiting for stock.",
scope="/customer/ravi", categories=["order"], importance=0.5)
for match in memory.recall("How does Asha want to be contacted?", limit=2, depth="shallow"):
print(round(match.score, 2), match.record.content)0.65 Asha prefers email, not phone calls. 0.61 Ravi's order C40 is waiting for stock.
Reading the two matches
- The top match shares "Asha" with the question and has the higher importance, so it ranks first.
- The second match is Ravi's order, which is not about Asha at all: counting words is a rough measure, and with every record new, a small difference decides the order.
- The score blends similarity, recency and importance.
depth="shallow"is a plain vector search with no model call. - Mixing customers is the problem scopes solve, which the next run shows.
Keeping customers apart with a scope
print(memory.tree())
for match in memory.recall("order status", scope="/customer/ravi", depth="shallow"):
print(match.record.content)/ (3 records)
/customer (3 records)
/customer/asha (2 records)
/customer/ravi (1 records)
Ravi's order C40 is waiting for stock.tree shows the scopes as a hierarchy with their record counts. A recall inside /customer/ravi cannot see Asha's records at all, which is how a desk keeps one customer's facts away from another's.
Pick one to watch it run, step by step.
Recall against a plain get
A dictionary lookup needs the exact key. Recall ranks by closeness, so a question that shares words with a fact still finds it.
| A dict get | Memory recall | |
|---|---|---|
| Match on | The exact key | Closeness of the vectors |
| Scope | One flat namespace | A tree you filter with scope= |
| Ranking | None; hit or miss | By similarity, recency and importance |
When a desk needs memory
- Carrying a customer's preference from one ticket to the next.
- Recalling an order a crew already looked up, without asking again.
- Keeping each customer's facts in their own scope so replies never cross.
remember and recall must use the same embedder, or the vectors are not comparable and recall returns noise. Build one embedder and pass it everywhere, and give each customer a scope so a recall never reads across.Related
- Previous: Knowledge sources: a reference library for agents
- Next: MCP server tools for an agent
- Reference: CrewAI docs, Memory
- Recall "When did A17 ship?" and compare the scores.
- Remember a fact without
scopeand printmemory.tree(). - Call
memory.forget(scope="/customer/ravi")and print the tree again.
You understood something today that you didn't yesterday.