> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Supermemory to Synap

> Move your Supermemory export into Synap with a ready-to-run script: map container tags onto Synap scopes, convert the export, verify the scope assignment, and ingest.

This guide moves a Supermemory export into Synap. It comes with a script that does the mechanical work, and explains the two decisions the script cannot make for you: **which scope each container tag belongs to**, and **what to do about memory quality**.

<Note>
  This is the hands-on procedure for Supermemory. For the method every migration shares — scope mapping, configuring your instance, verifying, and cutting over — see [How migration works](/migrations/how-it-works).
</Note>

## How Supermemory stores memory

Supermemory separates **documents** (the content you ingest) from **memories** (short facts extracted from those documents). Both are organised by **container tags** — flat string identifiers, each with its own isolated namespace.

Tags can encode structure by convention, such as `org:acme:user:john`, but they are not hierarchical: to Supermemory they are opaque strings. A tag holding one person's chat history and a tag holding your company handbook look identical.

That last point is the whole migration. Synap organises memory into three scopes — client, customer, and user — and nothing in a Supermemory export records which tag belongs where. You decide that, and this guide has you verify it twice before it becomes permanent.

## Before you start

<Steps>
  <Step title="Export your data from Supermemory">
    Download the export from your Supermemory dashboard. You get a single JSON file with a `documents` section (your original content) and a `memories` section (the facts Supermemory extracted from it).
  </Step>

  <Step title="Check the export is complete">
    Confirm `truncated` is `false` in **both** sections. If either says `true`, the export was cut short at its item limit — re-export before going further. The script warns you, but cannot recover content that is not in the file.
  </Step>

  <Step title="Configure your Instance for your agent">
    Extraction quality depends on your Instance's memory architecture, generated from the use-case file you supply at instance creation. If you created your Instance without one, add it **before** this import — otherwise your historical data is extracted with generic defaults, and you would re-import later to benefit from a tuned configuration. See [Memory Architecture](/concepts/memory-architecture).
  </Step>

  <Step title="Have a Synap Instance ready">
    You need an Instance and an API key; see [Quickstart](/getting-started/quickstart). Note whether your Instance treats customers and users as separate (B2B) or as the same thing (B2C) — you need this in step 2.
  </Step>
</Steps>

## Get the script

<Card title="supermemory_to_synap.py" icon="python" href="/migrations/supermemory_to_synap.py">
  Download the migration script
</Card>

`map` and `convert` run offline using only the standard library. `ingest` and `verify` talk to Synap through the SDK:

```bash theme={null}
pip install maximem-synap
```

## The migration, step by step

<Steps>
  <Step title="List your container tags">
    ```bash theme={null}
    python3 supermemory_to_synap.py map supermemory-export.json -o scope_map.json
    ```

    This reads the export and writes a `scope_map.json` listing every container tag it found, with the number of documents and memories in each:

    ```json theme={null}
    {
      "isolation": "b2b",
      "tags": {
        "org:acme:user:john": {
          "user_id": "john",
          "customer_id": "acme",
          "_documents": 128,
          "_memories": 941
        },
        "acme_handbook": {
          "user_id": "acme_handbook",
          "customer_id": null,
          "_documents": 12,
          "_memories": 87
        }
      }
    }
    ```

    Where a tag follows Supermemory's `key:value` convention, the script pre-fills a suggestion. Everything else is a guess you need to correct.
  </Step>

  <Step title="Assign a scope to every tag">
    Open `scope_map.json` and set `user_id` and `customer_id` for each tag.

    You never name a scope directly. Synap works out the scope from which identifiers you provide:

    | `user_id` | `customer_id` | Resulting scope            | Use for                                 |
    | --------- | ------------- | -------------------------- | --------------------------------------- |
    | set       | set           | **user**                   | One person's own history                |
    | not set   | set           | **customer**               | Shared across everyone at that customer |
    | not set   | not set       | **client**                 | Shared across your entire account       |
    | set       | not set       | Rejected on a B2B Instance | —                                       |

    Also set `isolation` to match your Instance: `b2b` if customers and users are separate, `b2c` if one customer is one user.

    In the example above, `acme_handbook` is company reference material, so its correct mapping is both fields `null` — client scope — not the `user_id` the script guessed.

    <Warning>
      Container tags are flat and mutually isolated, so nothing in the export distinguishes one person's history from company-wide material. A tag left at client scope by mistake becomes readable across your whole account. Step 4 has you check this before anything is sent.
    </Warning>
  </Step>

  <Step title="Convert the export">
    ```bash theme={null}
    python3 supermemory_to_synap.py convert supermemory-export.json \
        -m scope_map.json -o ./synap_import
    ```

    This writes files only and contacts no server:

    | File               | What it holds                                            |
    | ------------------ | -------------------------------------------------------- |
    | `batch_000.json` … | Your documents, ready to ingest, 25 per file             |
    | `baseline.json`    | Supermemory's own extracted memories, keyed by document  |
    | `report.json`      | Conversion stats and a reason for every skipped document |

    Open `report.json` and confirm `scope_breakdown` matches what you intended, and that `skipped` is empty or contains only documents you expect to lose.
  </Step>

  <Step title="Review the scope assignment">
    `report.json` gives you totals per scope. Before ingesting, check the assignment tag by tag against the actual content — the totals will look correct even when a tag is in the wrong place.

    ```python theme={null}
    import json, glob, collections

    tags = collections.defaultdict(list)
    for f in glob.glob("./synap_import/batch_*.json"):
        for d in json.load(open(f))["documents"]:
            tags[d["metadata"]["container_tag"]].append(d)

    for tag in sorted(tags):
        docs = tags[tag]
        m = docs[0]
        print(f"\n{tag}")
        print(f"  scope: {m['metadata']['synap_scope'].upper()}   "
              f"user_id={m['user_id']}  customer_id={m['customer_id']}  "
              f"documents={len(docs)}")
        for d in docs[:2]:
            print(f"    - {(d['metadata']['title'] or '(untitled)')[:70]}")
            print(f"      {d['document'][:90].replace(chr(10), ' ')}...")
    ```

    Read the samples, not just the scope labels. Two things to look for:

    * A tag at **client scope** whose samples are somebody's personal conversation. That content is about to become readable by every user on your account.
    * A tag at **user scope** whose samples read like policy, product, or reference documentation. That content will be copied into one person's memory instead of shared, and no one else will be able to retrieve it.

    Fix `scope_map.json` and re-run `convert` until every tag reads correctly. Nothing has been sent yet, so this loop is free.
  </Step>

  <Step title="Check how timestamps were resolved">
    `report.json` includes a `dct_sources` breakdown:

    * **`session_header`** — the original conversation date was recovered from the content. This is what you want.
    * **`created_at`** — no original date was available, so the document's upload date was used instead.

    Supermemory's `createdAt` records when a document was *uploaded to Supermemory*, not when the conversation happened. If you imported history into Supermemory, those two dates can be years apart. Memories dated from the upload date still work for recall, but questions like "what did I decide last spring?" answer against the wrong timeline.
  </Step>

  <Step title="Pilot one tag">
    Migrate a single tag first and confirm it behaves before committing the rest.

    Copy your scope map, keep one representative tag, and convert that alone into its own directory:

    ```python theme={null}
    import json

    full = json.load(open("scope_map.json"))
    tag = "org:acme:user:john"        # pick one real tag
    json.dump(
        {"isolation": full["isolation"], "tags": {tag: full["tags"][tag]}},
        open("pilot_map.json", "w"), indent=2,
    )
    ```

    ```bash theme={null}
    python3 supermemory_to_synap.py convert supermemory-export.json \
        -m pilot_map.json -o ./pilot
    python3 supermemory_to_synap.py ingest ./pilot
    python3 supermemory_to_synap.py verify ./pilot
    ```

    Every other tag will be reported as skipped during the pilot conversion. That is expected — they are not in `pilot_map.json`.

    Now run steps 9 and 10 against this one tag. Only continue once its isolation and retrieval both check out.
  </Step>

  <Step title="Ingest everything">
    Dry-run first to see what would be sent:

    ```bash theme={null}
    export SYNAP_API_KEY=your_key_here
    python3 supermemory_to_synap.py ingest ./synap_import --dry-run
    ```

    Then run it for real:

    ```bash theme={null}
    python3 supermemory_to_synap.py ingest ./synap_import
    ```

    The script records progress as it goes, so if it is interrupted you can re-run the same command and it resumes rather than sending anything twice. Each document is submitted with its original Supermemory ID, so re-running a batch does not create duplicates.

    <Tip>
      Your whole batch is checked against your quota before any of it is accepted. If you hit the limit, the script waits and retries automatically — you do not need to split the files yourself.
    </Tip>
  </Step>

  <Step title="Verify ingestion completed">
    Memories are built in the background, so ingestion finishing is not the same as memories being ready.

    ```bash theme={null}
    python3 supermemory_to_synap.py verify ./synap_import
    ```

    This waits for every submitted document and reports how many memories were created. Anything that did not complete cleanly is written to `incomplete.json`. A `partial_success` result means the document was processed but some of its memories were not stored; re-ingesting that document is safe.
  </Step>

  <Step title="Confirm scope isolation">
    This is the check that catches a wrong scope map. Do it on the pilot, and again after the full migration.

    Fetch context as one user and confirm nothing belonging to another user comes back:

    ```python theme={null}
    from maximem_synap import MaximemSynapSDK

    sdk = MaximemSynapSDK()
    await sdk.initialize()

    ctx = await sdk.user.context.fetch(
        user_id="john",
        customer_id="acme",
        search_query=["<a topic only another user discussed>"],
    )
    ```

    Choose a search term you know belongs to a *different* user's history. Nothing from that user should appear.

    Then check the reverse for anything you placed at client scope: fetch as two different users and confirm the shared material is retrievable by both. If it reaches only one, that tag is at user scope and needs correcting.
  </Step>

  <Step title="Spot-check retrieval coverage">
    Confirm the facts you expect actually come back:

    ```python theme={null}
    ctx = await sdk.user.context.fetch(
        user_id="john",
        customer_id="acme",
        search_query=["dietary preferences"],
    )
    ```

    `baseline.json` is useful here: it holds Supermemory's own extracted memories for each document, so you can sample a few and check they are retrievable from Synap too.

    Do not compare counts. One document does not become one memory, and the two systems extract differently by design — judge the migration by what you can retrieve.
  </Step>
</Steps>

## If the scope assignment was wrong

If you discover a misplaced tag after ingesting, correct it **one tag at a time**. Leave every other tag alone.

<Warning>
  Do not delete all of `ingestion_ids.json` and re-ingest the whole export. Ingestion recognises content it has already seen at the same scope for several days, so a blanket re-ingest returns the earlier result instead of rebuilding. The tags you deleted but did not re-scope would stay deleted. Only remove the memories belonging to the tag you are actually fixing.
</Warning>

**Step 1 — Delete only the affected tag's memories.** `ingest` writes `ingestion_ids.json` in the output directory, and each ingestion's status names the document it came from, which the batch files tie back to a container tag:

```python theme={null}
import json, glob
from uuid import UUID
from maximem_synap import MaximemSynapSDK

BAD_TAG = "acme_handbook"        # the tag you are correcting

# document_id -> container_tag, from the converted batches
doc_tag = {
    d["document_id"]: d["metadata"]["container_tag"]
    for f in glob.glob("./synap_import/batch_*.json")
    for d in json.load(open(f))["documents"]
}

sdk = MaximemSynapSDK()
await sdk.initialize()

removed = 0
for ingestion_id in json.load(open("./synap_import/ingestion_ids.json")):
    status = await sdk.memories.status(UUID(ingestion_id))
    if doc_tag.get(status.document_id) != BAD_TAG:
        continue                  # leave correctly-scoped tags untouched
    for memory_id in status.memory_ids:
        await sdk.memories.delete(UUID(memory_id))
        removed += 1

print(f"removed {removed} memories for {BAD_TAG}")
```

**Step 2 — Re-convert that tag alone.** Fix its entry in `scope_map.json`, then build a single-tag map and convert it into its own directory, exactly as in the pilot step:

```python theme={null}
import json

full = json.load(open("scope_map.json"))
json.dump(
    {"isolation": full["isolation"], "tags": {BAD_TAG: full["tags"][BAD_TAG]}},
    open("refix_map.json", "w"), indent=2,
)
```

```bash theme={null}
python3 supermemory_to_synap.py convert supermemory-export.json \
    -m refix_map.json -o ./refix
python3 supermemory_to_synap.py ingest ./refix
python3 supermemory_to_synap.py verify ./refix
```

Because the tag's scope has changed, this content is treated as new and is processed rather than matched against the earlier run. Re-run the isolation check from step 9 before moving on.

<Note>
  Keep every output directory until the migration is fully verified. `ingestion_ids.json` is the only record of which memories the migration created — without it, telling them apart from memories your live application has written since is difficult.
</Note>

## Why the script ingests documents, not memories

A Supermemory export contains both your original documents and the memories Supermemory extracted from them. The script deliberately ingests **the documents**.

Supermemory's memory entries are short summaries of your content. Importing them means Synap extracts from summaries rather than from what your users actually said — you inherit whatever the original extraction got wrong, and lose the detail it dropped. Because every memory in the export points back to the document it came from, you can re-extract from the original source instead, which is almost always better.

There is a second, more important reason to re-extract. Every Synap instance runs its own **Memory Architecture Configuration ([MACA](/concepts/memory-architecture))** — a per-instance memory policy generated from the use-case file you provide when you create the instance. It governs what gets extracted and how, tuned to your agent's domain and audience.

Re-extracting your imported documents through that configuration means your migrated history is processed by the *same rules as your live traffic*. From the very same source conversations, a support agent's instance surfaces issues and resolutions, while a companion agent's instance surfaces preferences and emotional context. Importing Supermemory's pre-extracted memories would bypass this entirely and leave your historical data shaped by generic rules that do not match your agent — so re-ingestion is not overhead, it is how your old data starts behaving as if your agent created it all along.

There is one case where re-extraction is not possible: documents brought in through a Supermemory connector sometimes keep no local copy of their text. Those appear in `report.json` as `empty content in export`. Re-sync the connector on the Supermemory side and export again, or accept the loss.

The extracted memories are still useful — that is what `baseline.json` is for. Use them to check your coverage after migrating, not as the thing you migrate.

<Note>
  Supermemory's memory entries are typically a single short sentence. Content that brief carries too little signal to memorise on its own and is discarded on arrival, so importing those strings directly would quietly lose most of them. The script checks length locally and reports anything at risk rather than letting it disappear.
</Note>

## What you gain

* **Typed extractions** — facts, preferences, episodes, emotions, and temporal events as separate lists, rather than one undifferentiated pool of strings.
* **Three scopes, not one flat namespace** — client, customer, and user, with roll-up between them, so shared knowledge is stored once instead of copied into every tag.
* **Entity resolution across conversations** — the same person or product recognised across sessions.
* **Context compaction** — long histories stay usable without you managing the window.

## What you'll need to adapt

Some Supermemory structure is not present in a dashboard export, and no migration can recover it:

* **Version history and superseded facts.** The export contains only current memories, without their revision chains.
* **Relationships between memories.** Supermemory's `updates` / `extends` / `derives` links are not included.
* **Inferred-fact flags.** There is no way to tell which memories Supermemory derived rather than observed.
* **Embeddings.** Vectors are never exported by either system; Synap generates its own during ingestion.

None of this is a real loss, because re-extracting from your original documents rebuilds the equivalent structure natively — Synap tracks its own memory lineage and relationships as it ingests.

## Troubleshooting

<AccordionGroup>
  <Accordion title="convert skipped every document">
    Your scope map still has unfilled entries. On a B2B Instance a tag with a `user_id` but no `customer_id` is rejected, which is deliberate — it prevents documents landing in an unintended scope. Set `customer_id` for those tags, or switch `isolation` to `b2c` if that matches your Instance.
  </Accordion>

  <Accordion title="A tag is missing from the scope map">
    `map` only lists tags that appear in the export. If a tag exists in Supermemory but has no documents or memories in the file, it will not appear — and nothing needs migrating for it.
  </Accordion>

  <Accordion title="Documents skipped as 'empty content in export'">
    These came from a Supermemory connector that kept no local copy of the text. Re-sync the connector in Supermemory and export again.
  </Accordion>

  <Accordion title="Documents skipped as 'session-date header present but unparseable'">
    The document announces a session date the script could not read. This is treated as an error rather than silently falling back to the upload date, because a wrong date is worse than a skipped document. Report the format and we will add it.
  </Accordion>

  <Accordion title="Fewer memories than documents ingested">
    Expected. One document does not become one memory — long conversations are split, and content with nothing worth remembering produces none. Judge the migration by what you can retrieve, not by counting rows.
  </Accordion>

  <Accordion title="Ingestion stopped partway through">
    Re-run the same `ingest` command. Completed batches are recorded and skipped, so it resumes rather than re-sending.
  </Accordion>
</AccordionGroup>

## After you cut over

Work through the shared [migration method](/migrations/how-it-works), which covers the parts common to every source: configuring your instance so extraction quality is good from day one, deciding your `conversation_id` strategy, swapping your retrieval call sites, and adding graceful degradation.

Once you are cut over, retire the old service. Do not dual-write — diverging memory state is a harder problem than a clean cutover.
