Migrating from Zep
Zep attaches each conversation thread to a user, and that user’s knowledge graph collects facts across threads. MemorySync has the same two ideas under different names: who a memory belongs to, and which conversation produced it. Mapping them is the one step worth getting right before you start.
How the concepts map
Key differences
| Zep | MemorySync | What it means for your code |
|---|---|---|
user.add() and thread.create() before writing | No setup call | Scope is a header on the write itself. |
thread.add_messages(thread_id, messages=[...]) | POST /v1/history/append or POST /memory/add | Both extract facts from what you send; history append also keeps the transcript for your app to read back. |
graph.search(user_id=..., query=...) or thread.get_user_context(thread_id) | POST /memory/query with query and k | Reads span the end user, and session_id narrows when you want that. |
| A thread’s messages | POST /v1/history/list for the transcript; GET /memory/export for the facts | The transcript and the facts are kept apart, so reading one never returns the other. |
| Messages with roles | Records with source and metadata | Role becomes descriptive context you can filter on. |
Install the client
pip install memorysync
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",project_id=os.environ.get("MEMORYSYNC_PROJECT_ID") or None,end_user_id="user-123",)
Operation by operation
from zep_cloud.types import Messageclient.user.add(user_id="user_123")client.thread.create(thread_id="thread_42", user_id="user_123")client.thread.add_messages("thread_42", messages=[Message(name="Alice", role="user", content="I love Python for backend work"),])
results = client.graph.search(user_id="user_123",query="programming",limit=3,)
Move your episodes with one command
The CLI reads a Zep export, or walks your account through the API — users first, then each user’s episodes — and sends every episode into that user’s scope through the same fact extraction as POST /memory/bulk-add, carrying the thread id through as session_id. Only the durable facts in each episode are stored, never the episode text as a transcript row. Start with --dry-run: it reports the per-user breakdown, the user and assistant split, and everything it would skip, without writing anything.
# See what would happen. Writes nothing.npx memorysync-cli migrate zep --file zep_export.json --dry-run# Do it.npx memorysync-cli migrate zep --file zep_export.json
| What it does | Why |
|---|---|
| Migrates episodes, not the derived graph | Zep keeps episodes verbatim as the original source, alongside the nodes and edges it derives from them. The graph is Zep’s own representation and has no equivalent here, so porting it would mean holding a model nothing reads. |
Writes each episode under its own user, and carries thread_id into session_id | This is the split described above, applied. The user is the isolation boundary; the thread only narrows a query. |
| Sends both sides of the conversation and reports the split | An assistant episode is what the model said. Like every imported record it goes through fact extraction, so only the durable facts in it are kept, filed under the user — right for some products, wrong for others. Dropping it by default would be silent data loss, so you get the counts and --role user if you want only the human turns. |
Skips json and fact_triple episodes, and reports how many | A fact triple is a serialised graph edge and JSON is a business record. Stored as memory text they are strings retrieval can match but never usefully explain. --include-source json,fact_triple migrates them anyway. |
| Skips deleted users | Their episodes are data the customer asked you to remove. Migrating it would resurrect it somewhere new. |
Keeps role, role_type, source and the original timestamp in metadata | Who said it is context about a record, not part of its identity, so it stays filterable without becoming the scope. |
Sends every episode uuid as client_ref | Re-running is safe. Episodes that already landed are reported as already migrated rather than stored twice. |
Or move them yourself
The command above is these steps, done for you. Do it by hand when you are building it into your own pipeline rather than running a one-time migration.
- 1List the threads you want to bring across (sessions, in older Zep SDKs), and read each thread’s user id. That id becomes the end-user scope.
- 2Decide whether to send the assistant’s messages too. Every message goes through fact extraction, so small talk is skipped either way, but facts the assistant stated would be filed under the user.
- 3Import per session, carrying the role in
metadataand the session id insession_id. - 4Check the per-item results, then query as one migrated user to confirm the scope is right.
import osfrom memorysync import MemorySyncClient# messages and user_id come from your Zep exportdef import_session(session_id: str, user_id: str, messages: list[dict]) -> None:client = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",project_id=os.environ.get("MEMORYSYNC_PROJECT_ID") or None,end_user_id=user_id,)items = [{"text": message["content"],"source": "import",# the uuid makes re-running safe; without it a retry stores a second copy"client_ref": message.get("uuid"),"metadata": {"role": message.get("role"), "zep_uuid": message.get("uuid")},}for message in messagesif message.get("content", "").strip()]result = client.bulk_add(items)print(session_id, result.created, "created,", result.skipped, "skipped")