CrewAI
CrewAI runs a crew of
role-playing agents that call tools to get work done. This page gives those
agents a memory that lasts beyond one run, through the Korely Python
SDK. You write a few custom
tools, each wrapping one SDK call, and attach them to an
Agent. From then on the model decides when to recall, when to
save and when to search.
The recall tool uses get_context. It does not hand the model a
pile of rows to sift. It returns a
context block: the
facts that are true now (for example
"Luca upgraded to Pro", "prefers async standups") plus the best-matching
memories. A fact that a newer one
replaced is closed and does not appear.
What the tools do
Each tool calls one Korely method. Recall is the main read: it assembles the current facts. Search finds the exact memory that mentioned something. Save stores a new memory. There is no schema to write and no glue code beyond the tool bodies below.
Install
You need Python 3.10 to 3.13 (CrewAI's supported range), CrewAI and the
Korely Python package. The examples were run with crewai
1.15.33 and korely-memory 0.1.28, with a fake model server, so no model provider was called.
pip install crewai korely-memory
The custom-tool primitives, BaseTool and the
tool decorator, ship in the core crewai package
under crewai.tools. CrewAI's library of pre-built tools
(search, scraping and so on) lives in the separate crewai-tools
package and is not required here.
You also need a Korely API key. Sign
up at korely.ai, copy the kor_live_ key from
the dashboard, and export it. CrewAI's own model needs its provider key
too (for the default OpenAI model, OPENAI_API_KEY):
export KORELY_API_KEY="kor_live_..."Define the memory tools
Subclass BaseTool for typed, validated input: each tool
declares an args_schema (a Pydantic model), a clear
description the agent reads to decide when to use it, and a
_run method that does the work. We define three, recall, save
and search, each wrapping one Korely call.
from typing import Type
from crewai.tools import BaseToolfrom pydantic import BaseModel, Field
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environmentAGENT_ID = "crewai-crew" # whose memory this is: a write names a user_id or an agent_id
# ---- recall: the main read. Reach for this first. -------------------------
class RecallInput(BaseModel): """Input schema for RecallMemoryTool.""" query: str = Field(..., description="What you want to recall about the user.")
class RecallMemoryTool(BaseTool): name: str = "Recall Memory" description: str = ( "Recall settled facts and relevant memories about the user. " "Returns an assembled context block, not raw rows. " "Use this before answering anything about the user's past." ) args_schema: Type[BaseModel] = RecallInput
def _run(self, query: str) -> str: ctx = korely.get_context(query=query, agent_id=AGENT_ID, token_budget=800) return ctx.context or "No memory yet."
# ---- save: write something worth remembering ------------------------------
class SaveInput(BaseModel): """Input schema for SaveMemoryTool.""" content: str = Field(..., description="A durable fact worth remembering.")
class SaveMemoryTool(BaseTool): name: str = "Save Memory" description: str = ( "Save a durable fact to memory. Use when the user tells you " "something worth remembering for later turns or sessions." ) args_schema: Type[BaseModel] = SaveInput
def _run(self, content: str) -> str: memory = korely.add(content, agent_id=AGENT_ID) return "Saved as " + memory.id
# ---- search: targeted lookup of an exact memory ---------------------------
class SearchInput(BaseModel): """Input schema for SearchMemoryTool.""" query: str = Field(..., description="Keyword-style query, 1 to 5 words.")
class SearchMemoryTool(BaseTool): name: str = "Search Memory" description: str = ( "Find the exact memory that mentioned something. " "Use when recall is not specific enough and you need the raw snippet." ) args_schema: Type[BaseModel] = SearchInput
def _run(self, query: str) -> str: hits = korely.search(query, agent_id=AGENT_ID, limit=5) if not hits: return "No matching memories." return "\n".join("- " + (hit.snippet or "") for hit in hits) Why get_context for recall. A search
returns matching snippets that the model still has to read and reconcile.
get_context goes one step further and returns the current
facts, already reconciled, in one block. The returned
Context object has .context (the text you hand
to the model), .tokens (its size) and .sources
(the ids of the facts and memories behind it). Keep search for "find the
exact memory that mentioned X".
Attach the tools and run
Instantiate the tools, attach them to an Agent via
tools=[...], wrap the work in a Task, and run the
Crew. The agent decides when to call each tool. You nudge it
through the agent's goal and backstory:
from crewai import Agent, Task, Crew, Process
from korely_tools import RecallMemoryTool, SaveMemoryTool, SearchMemoryTool
recall = RecallMemoryTool()save = SaveMemoryTool()search = SearchMemoryTool()
assistant = Agent( role="Personal Assistant", goal="Answer using what you already know about the user.", backstory=( "You have a persistent memory. Always recall before answering " "questions about the user's past, and save anything durable they tell you." ), tools=[recall, save, search], verbose=True,)
answer = Task( description="Answer this question about the user: {question}", expected_output="A short, specific answer grounded in memory.", agent=assistant,)
crew = Crew( agents=[assistant], tasks=[answer], process=Process.sequential, verbose=True,)
result = crew.kickoff( inputs={"question": "What did I decide about the lease renewal?"})print(result)
When you run it, the agent calls Recall Memory, which calls
korely.get_context, and reasons over the block with its own
model. The memories and the answer below are an example, not your output:
$ python crew.py
→ tool call Recall Memory(query="lease renewal decision")
← korely.get_context returns an assembled block:
_The facts below are a compact profile of the user; [...]_
## Known facts
- user renewed apartment lease (since 2026-05-28)
- Anna confirmed rent increase 3% (since 2026-05-28)
## Relevant memories
_A time in square brackets after a relative expression [...]_
- [2026-05-28] Renewal deadline is July 1. Anna confirmed a 3% increase,
1,200 to 1,236 EUR per month. She wants the signed copy by post.
Agent › You decided to renew. The deadline is July 1: Anna
confirmed a 3% increase to 1,236 EUR per month, and she wants the
signed copy by post.
The block that get_context returns is assembled from stored
facts and memories, not written by a model. No language model runs when
Korely answers a read; the CrewAI agent's own model does the reasoning.
Reads are cheap on our side, so every plan allows at least 100 times more
queries than writes per month (see the pricing page).
The lightweight @tool decorator
If you do not need a typed args_schema, the tool
decorator is the shortest path. The decorated function
must have a docstring. CrewAI raises a
ValueError without one, and the docstring is what the agent
reads to decide when to call it:
from crewai.tools import toolfrom korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environmentAGENT_ID = "crewai-crew" # whose memory this is: a write names a user_id or an agent_id
@tool("Recall Memory")def recall(query: str) -> str: """Recall settled facts and relevant memories about the user.""" return korely.get_context(query=query, agent_id=AGENT_ID, token_budget=800).context or "No memory yet."
@tool("Save Memory")def save(content: str) -> str: """Save a durable fact to memory.""" return "Saved as " + korely.add(content, agent_id=AGENT_ID).id
Pass the decorated functions straight into Agent(tools=[recall,
save]). They are Tool objects, which an
Agent accepts like a BaseTool subclass.
One memory per end user
The tools above read and write one memory, the crew's own, named by
agent_id (every write names an owner: a user_id or an agent_id
is required). That suits a personal assistant or an internal crew. When
each person who uses your product needs a private memory, pass a
user_id on every call, and optionally the other labels.
user_id is the end user
(unlimited on every plan; you choose the string),
agent_id is your
application, and run_id
is one session. A read without user_id covers every end user
of the project.
Bind user_id into the tool when you build it, on your server,
so the model cannot reach another person's memory whatever the
conversation says:
from typing import Type
from crewai.tools import BaseToolfrom pydantic import BaseModel, Field
from korely_memory import Korely
korely = Korely() # reads KORELY_API_KEY from the environment
class RecallInput(BaseModel): query: str = Field(..., description="What you want to recall about this user.")
class ScopedRecallTool(BaseTool): name: str = "Recall Memory" description: str = "Recall everything known about this specific user." args_schema: Type[BaseModel] = RecallInput
# user_id is bound at construction, on your server. The model never sets it. user_id: str
def _run(self, query: str) -> str: ctx = korely.get_context( query=query, user_id=self.user_id, agent_id="support-bot", token_budget=800, ) return ctx.context or "No memory yet."
# Per request, bind the end user from the session:recall = ScopedRecallTool(user_id="customer-4812")
A read that carries a user_id returns only that user's data
(we checked this with two users on the Cloud). Quotas count writes and
queries, never people.
Facts arrive after the write. korely.add(content, agent_id=AGENT_ID) returns at once with the stored memory.
The facts are written by
extraction, usually about ten
seconds later. A fact you just wrote may therefore be missing from the
next get_context or get_facts call in the same
turn; read again a little later. A search finds the memory itself right
away.
Troubleshooting
| Symptom | Fix |
|---|---|
ImportError on crewai.tools |
The canonical import is from crewai.tools import BaseTool
and from crewai.tools import tool. Older tutorials use
from crewai_tools import BaseTool; prefer
crewai.tools from the core package. Do not use
from crewai import tool, the tool decorator
is not a top-level export.
|
ValueError: Function must have a docstring |
A @tool-decorated function needs a docstring; CrewAI
uses it as the tool description. Add one line describing what the
tool does.
|
401 Unauthorized from api.korely.ai |
Key missing or revoked. Check that KORELY_API_KEY is set
in the environment and starts with kor_live_. You can
also pass it explicitly: Korely(api_key="kor_live_...").
|
| The agent never calls the memory tools |
The model chooses its tools. Put the rule in the agent's
backstory ("always recall before answering") and keep
each tool's description short and about the action.
Smaller models need the nudge more than larger ones.
|
get_facts returns nothing right after a write |
The facts are written about ten seconds after add, so a
memory you just wrote may not have facts yet. Read again a little
later. A search finds the memory itself immediately.
|
Where to go next
- Memory as a tool: the pattern behind this page, for any framework, end to end.
- Get context: how the
recall block is built, what
token_budgetcontrols, and what comes back in.sources. - Python SDK reference: the full
add/search/get_context/get_facts/get_profilesurface.
Something not working? Email
[email protected] with your
crewai and korely-memory versions and the
traceback. We read every message.