MemorySync

Migrate From Mem0, Zep or Supermemory With a Dry Run

Move existing memories into MemorySync with one CLI command per provider: preview with --dry-run, land each record under its own end user, and re-run safely.

Published on 4 min read

Migrate From Mem0, Zep or Supermemory With a Dry Run
On this page

Moving memory between providers has one way to go badly wrong that is easy to miss: records land in the wrong scope. An export usually holds many users. If they all end up under one end user, or under none, the import reports success and every user's assistant quietly forgets them.

The MemorySync CLI has a migrate command for Mem0, Zep and Supermemory that is built around that risk. It groups records by the user they belong to, writes each group into that user's scope, refuses records it cannot place, and shows you all of it with --dry-run before it writes anything.

Install the CLI

The Node and Python CLIs are interchangeable and share stored keys. Use whichever runtime you have:

Shell
# Node
npx memorysync-cli --help
# Python
pipx run --spec memorysync-cli memorysync --help

Run memorysync init once to store your MemorySync key, or set MEMORYSYNC_API_KEY in the environment, which is the better choice in CI. When you pull directly from the old provider's API, its key is passed separately with --key.

From Mem0

Start from a Mem0 export file, or let the CLI read the account directly with your Mem0 key:

Shell
# See what would happen. Writes nothing.
memorysync migrate mem0 --file mem0_export.json --dry-run
# Do it.
memorysync migrate mem0 --file mem0_export.json
# Or pull straight from Mem0 (your Mem0 key, not your MemorySync one):
memorysync migrate mem0 --key $MEM0_API_KEY --dry-run
memorysync migrate mem0 --key $MEM0_API_KEY --source-user alice

What the command guarantees:

  • Records are grouped by their Mem0 `user_id` and each group is written under that end user.
  • Every Mem0 id is sent as `client_ref`. Re-running is safe: records that already landed are reported as already migrated instead of being stored twice, so a run that stopped halfway is finished by running it again.
  • A record whose user cannot be determined is refused, not filed in a fallback scope where nobody would look. Pass --source-user <id> to state the scope, or re-export with user_id.
  • Nothing is discarded. Mem0 categories become tags, and id, created_at, agent_id, app_id and run_id are kept in metadata, so the two systems stay reconcilable on metadata.mem0_id.
  • Writes are batched at 50 records per request, the limit bulk add enforces.

One detail to plan for: Mem0's created_at is kept as metadata.mem0_created_at, and MemorySync assigns its own created_at. Recency feeds ranking, so back-dating would push every migrated memory below anything written afterwards. Sort or filter on the metadata field when you need the original order.

From Zep

The CLI reads a Zep export, or walks your account through the API, users first and then each user's episodes:

Shell
memorysync migrate zep --file zep_export.json --dry-run
memorysync migrate zep --file zep_export.json
# Through the API, with your Zep key:
memorysync migrate zep --key $ZEP_API_KEY --dry-run
# Only what the human said, not the assistant:
memorysync migrate zep --key $ZEP_API_KEY --role user
# One user at a time:
memorysync migrate zep --key $ZEP_API_KEY --source-user user_123
  • Episodes are migrated, not the derived graph. Zep keeps episodes verbatim alongside the graph it builds from them. The episodes are the source; the graph has no equivalent here.
  • Each episode lands under its own user, and the Zep thread id becomes session_id. The user is the isolation boundary; the session only narrows a query.
  • Both sides of the conversation are sent by default, and the dry run reports the user and assistant split. --role user keeps only what the human said, which usually retrieves better.
  • `json` and `fact_triple` episodes are skipped and counted, because a serialised graph edge or a business record does not read as a memory. --include-source json,fact_triple migrates them anyway.
  • Deleted users are skipped. Their data is what someone asked you to remove.
  • Every episode uuid is sent as `client_ref`, so re-runs are safe here too.

From Supermemory

Shell
memorysync migrate supermemory --file supermemory_export.json --dry-run
memorysync migrate supermemory --file supermemory_export.json
# Through the API, with your Supermemory key:
memorysync migrate supermemory --key $SUPERMEMORY_API_KEY --dry-run
# If your scope tags use a different convention:
memorysync migrate supermemory --key $SUPERMEMORY_API_KEY --scope-tag-prefix tenant_

Supermemory uses container tags both as the isolation boundary and as labels. The CLI treats the tag starting with user_ as the end user and every other tag as a label, and --scope-tag-prefix changes the prefix. A document matching two scope tags, or none, is refused and named in the report. A document's summary is never stored in place of its content, and documents still being processed at the source are skipped and counted.

What a migrated record becomes

Every record goes through the same fact extraction as any other write. A long note can become several memories, and small talk becomes none. A one-value fact, such as where someone lives, is replaced when a newer value arrives, rather than kept beside the old one.

That is also why importing fewer, better records works best. A provider that stored whole turns hands you a lot of text that is not a fact worth keeping. The dry run shows the split before you commit.

Change the calls

After the move, the calls map closely. On the MemorySync side the end user is set once on the client, instead of being passed as a filter on each call:

Python
import os
from memorysync import MemorySyncClient
client = MemorySyncClient(
api_key=os.environ["MEMORYSYNC_API_KEY"],
base_url="https://api.memorysync.io",
end_user_id="alice", # was a user_id filter on each call
)
client.add("The user prefers dark mode.", source="chat")
result = client.query("user preferences", k=5) # search -> query, top_k -> k
for memory in result.memories:
print(memory.text)

A Zep thread maps to session_id on add and, optionally, on query when you want results from one conversation only.

Next step

Run the dry run first and read the per-user breakdown. The migration pages for Mem0, Zep and Supermemory map every operation, and the MemorySync vs Mem0 comparison lists the differences with a source for each.

Share article

Blog

Try it on your own workload

The free tier stores real memories and returns real rankings. One write and one retrieval to start.

Start free. No credit card required.