CrewAI
One command writes memory into a CrewAI crew as code you own - CrewAI's own memory, kept in twin memory - with the end user set by your code, never the model.
twinmemory-cli adds memory to a CrewAI crew the way shadcn adds a component: it writes one file into your
project, and from then on that file is yours to read and edit. It is on PyPI Soon: this page describes
version 0.1.0, and until it is published, a tool over REST does the job.
uvx twinmemory-cli init crewai # a crew you have
uvx twinmemory-cli create crewai my-crew # a new one, with memory wired in
uvx twinmemory-cli doctor # is it all set up?CrewAI 1.15 or newer, Python 3.10 to 3.13. The file uses the Python SDK. CrewAI is
Python only, so there is no npx version of this command.
Python 3.10 and uv
CrewAI needs onnxruntime, whose releases from 1.24 on have no wheel for Python 3.10. uv can choose one
anyway and then fail to install it. On 3.10, add onnxruntime<1.24 to your project first (uv add 'onnxruntime<1.24'), or use Python 3.11 or newer. pip picks a version that installs by itself.
What init does
Before it writes anything, init crewai checks two things, and either can stop it with nothing changed:
- it is in a CrewAI crew: a classic one (
@CrewBaseinsrc/<package>/crew.py) or a JSON one (crew.jsonc, whatcrewai createmakes by default). A CrewAI Flow is not supported yet; - the installed CrewAI is 1.15 or newer. A newer CrewAI than this release was tested with works, with a
warning: if tasks stop recalling or saving memory, look for warnings on the
twinmemorylogger. Thecreatecommand checks the CrewAI it installs the same way.
Then it writes memory.py beside the crew, adds twinmemory with the project's own tool, and sets up your
key as init langgraph does: --key tm_..., or paste it when asked, and Enter skips it. The same flags
apply - --fast, --key, --base-url, --skip-key, --dir, --file, --force, --no-install,
--dry-run, --yes - and --tools-only, below.
What it writes
TwinMemory is CrewAI's own memory, kept in twin memory. Give it to your crew, and CrewAI:
- recalls the user's memory before every task, by the task's description, and puts it in the prompt;
- saves what each task found, after it, under the agent's role;
- gives every agent its Search memory and Save to memory tools, backed by twin memory.
It skips the model call CrewAI makes to pick facts out of a task's result: twin memory reads what it is given
itself. Its settings are at the top of the file: FAST, STRICT, MAX_CHARS and TIMEOUT. Recall and
save run in the task's own thread, so a task waits for memory, for up to TIMEOUT a call; under akickoff,
so does the event loop.
Wire it in
from crewai import Crew
from crewai.project import CrewBase, before_kickoff, crew
from my_crew.memory import TwinMemory, whose
@CrewBase
class MyCrew:
memory = TwinMemory()
@before_kickoff
def whose_memory(self, inputs):
return whose(inputs)
# your agents and tasks, as before
@crew
def crew(self) -> Crew:
return Crew(agents=self.agents, tasks=self.tasks, memory=self.memory)Then run it with the end user's id:
MyCrew().crew().kickoff(inputs={"user_id": "dana", "topic": "a weekend in Porto"})- Whose memory is the run's
inputs["user_id"], set by your code; the model never chooses it. Every kickoff must carry it, and a run without it stops before any task. See End users. - One memory serves every run. The user is held per run, not on the memory object, so concurrent runs
for different users -
kickoff_for_each_async,akickoff- never mix. - The key is checked at kickoff, once per process: CrewAI hides memory's errors from you, so a wrong key stops the run there instead of quietly switching memory off.
- Saving needs a key that writes.
When memory is down
The file fails open: with STRICT = False, the default, memory being unreachable, slow or failing costs a
task its memory, not the run. Recall comes back empty, a save is skipped, and each failure is logged on the
twinmemory logger, so it shows in your logs even though CrewAI swallows it. A wrong key still stops the run
at kickoff.
Tools only
init crewai --tools-only writes no TwinMemory: for a crew that does not use CrewAI's memory, or to choose
yourself when memory is read and saved. The user is fixed by your code when the tools are made.
from my_crew.memory import memory_tools, save_result
planner = Agent(role="Travel planner", goal="...", backstory="...", tools=memory_tools("dana"))
result = MyCrew().crew().kickoff(inputs={"topic": "a weekend in Porto"})
save_result("dana", result) # keep what the crew foundmemory_tools(user_id, writes=False) leaves out Save to memory, for a read key.
create
create crewai <name> makes a classic crew with memory wired in: one assistant agent and its task, and a
run_crew command. --model takes CrewAI's provider/model name (default anthropic/claude-sonnet-5-5) and
adds the CrewAI extra that provider needs.
uvx twinmemory-cli create crewai my-crew --key tm_...
cd my-crew
uv run run_crew --user dana "I just moved to Lisbon for a job at Acme."
uv run run_crew --user dana "Where do I live these days?" # it remembersdoctor
doctor checks the memory file, the dependencies and their installed versions (CrewAI against the version
this release was tested with), the key (with one free request), and that .env is not committed. Each
failure says how to fix it.
Until it is published
A custom tool over REST gives an agent the user's memory, and your backend adds the result after the crew runs. The tool is made per end user, so the user id comes from your code.
import os
import requests
from crewai.tools import tool
TWIN = "https://api.twinmemory.dev/v1/memory"
def twin(user_id: str, route: str, body: dict) -> dict | None:
r = requests.post(
f"{TWIN}/{route}",
headers={"Authorization": f"Bearer {os.environ['TWIN_MEMORY_API_KEY']}", "X-Twin-Account": user_id},
json=body,
timeout=60,
)
if r.status_code == 404: # no memory for this end user yet
return None
r.raise_for_status()
return r.json()
def memory_tool(user_id: str):
@tool("Search memory")
def search_memory(question: str) -> str:
"""Search what this user has said before."""
brief = twin(user_id, "search", {"query": question}) or {}
lines = [*brief.get("known", []), *(f["text"] for f in brief.get("facts", []))]
lines += [h["text"] for h in brief.get("hits", [])]
return "\n".join(lines) or "Memory holds nothing about this."
return search_memoryAfter the run: twin(user_id, "add", {"items": [{"kind": "note", "text": result.raw}]}).
Checked against docs.crewai.com/concepts/memory and CrewAI 1.15.28 on 2026-10-23.
LangGraph
One command writes memory into a LangGraph or LangGraph.js project as code you own - nodes that recall and remember, and tools the model calls - with the end user set by your code, never the model.
OpenAI Agents SDK
One command writes memory into an OpenAI Agents SDK project as code you own - recall before the model, remember after the run, and tools the model calls - with the end user set by your code, never the model.