Migrating from Mem0
Move your memories and your calling code from Mem0 to MemorySync. The import runs through one endpoint, and you can keep both providers live until your own queries look right.
Key differences
| Mem0 | MemorySync | What it means for your code |
|---|---|---|
user_id passed per call | X-End-User-ID request header | Isolation is enforced by the API, so a read cannot be widened by a forgotten filter argument. |
filters={"user_id": ...} on search | Scope header, not a filter | filters is left free for your own tags and metadata. |
messages=[...] extracted into facts | text — one statement per record | You decide what is worth storing, so what comes back is what you put in. |
top_k | k | Same meaning, shorter name. |
| Org and project ids on the client | X-Project-ID request header | One key can serve several projects without new client instances. |
| No update on the managed API | PATCH /memory/{memory_id} | Tags, importance, metadata, and source can be corrected in place. |
Install the client
python -m pip install memorysync==1.9.2
import osfrom memorysync import MemorySyncClientclient = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",project_id=os.environ["MEMORYSYNC_PROJECT_ID"],end_user_id="user-123",)
Operation by operation
client.add(messages="User prefers dark mode",user_id="alice",)
results = client.search(query="user preferences",filters={"user_id": "alice"},top_k=5,)
memories = client.get_all(filters={"user_id": "alice"})client.delete(memory_id="mem_123")
Where Mem0 returns an extracted fact it decided to keep, MemorySync returns the record you wrote. If you were reconciling extraction output against your own database, that reconciliation step disappears.
Move your memories with one command
The CLI reads a Mem0 export, or pulls the account straight from Mem0, and writes every record into the end-user scope it belongs to. Start with --dry-run: it reports exactly what would be written, per user, and writes nothing.
# See what would happen. Writes nothing.npx memorysync-cli migrate mem0 --file mem0_export.json --dry-run# Do it.npx memorysync-cli migrate mem0 --file mem0_export.json
| What it does | Why |
|---|---|
Groups records by their Mem0 user_id and writes each group under that end-user scope | One export usually holds many users. Each has to land in its own scope or it is unreachable by the person it belongs to. |
Sends every Mem0 id as client_ref | Re-running is safe. Records that already landed are reported as already migrated instead of being stored a second time, so a run that stopped halfway is finished by running it again. |
| Refuses any record whose user cannot be determined | Writing it into a fallback scope would file it where nobody looks, and there would be no error to notice. Pass --source-user <id> to state the scope, or re-export including user_id. |
Maps categories to tags, and keeps id, created_at, agent_id, app_id and run_id in metadata | Nothing from the source is discarded, and the two systems stay reconcilable on metadata.mem0_id. |
| Batches at 50 records per request | The ceiling bulk-add enforces. The command handles the chunking. |
Or move them yourself
The command above is the same four steps, done for you. Do it by hand when you are building it into your own pipeline rather than running a one-time migration.
- 1Export from Mem0. The dashboard export is the least code; the export API works too, and returns records with
id,content,metadata, andcreated_at. - 2Group the exported records by their Mem0
user_id. Each group becomes one MemorySync end-user scope. - 3Import each group with bulk add, keeping the original id in
metadataso the two systems stay reconcilable. - 4Read the per-item results. A
207with skipped items is a normal outcome, not a failure.
import json, osfrom memorysync import MemorySyncClientwith open("mem0_export.json") as handle:export = json.load(handle)# one MemorySync client per end user keeps the scope explicitby_user: dict[str, list[dict]] = {}for record in export["memories"]:if record.get("content"):by_user.setdefault(record.get("user_id", "unknown"), []).append(record)for user_id, records in by_user.items():client = MemorySyncClient(api_key=os.environ["MEMORYSYNC_API_KEY"],base_url="https://api.memorysync.io",project_id=os.environ["MEMORYSYNC_PROJECT_ID"],end_user_id=user_id,)result = client.bulk_add([{"text": record["content"],"source": "import","client_ref": record.get("id"),"metadata": {"mem0_id": record.get("id"), **(record.get("metadata") or {})},}for record in records],)print(user_id, result.created, "created,", result.skipped, "skipped")
Worth knowing before you cut over
- Writes are durable when
addreturns. There is no event id to poll and no queued state to wait through before a memory is retrievable. - Nothing is silently rewritten. A record changes only when you call update or forget, so your memory count reflects your own decisions.
- A memory belongs to one end user. Cross-user reads are not a filter you have to remember to apply.
- Retrieval returns whole records with their metadata, not fragments to reassemble.
- Keep your Mem0 export until you have run a full usage cycle here. It is the cheapest rollback available.