Import
Import pulls an existing knowledge base into a space in a single upload: claude.ai and ChatGPT exports, an MCP memory-server, Claude projects and memory, and plain Markdown and text files. The format is detected by content, not by file name, so you don't have to prepare anything by hand — the engine parses the archive on its own and sorts it into folders.
Import mirrors export: export reads the source-of-truth files from disk, while import parses the source and writes notes through the normal write-path. Each note lands in the space exactly as if a human had created it in the editor — with versioning, provenance, and indexing.
What formats it understands
A single Claude or ChatGPT archive usually holds several kinds of data at once; import detects and brings over everything it knows how to handle:
| Source | File in the export | What becomes a note |
|---|---|---|
| Claude conversations | conversations.json | One note per conversation, messages as ### Human/Assistant |
| ChatGPT conversations | conversations.json (including sharded conversations-000.json…) | One note per conversation, transcript in time order |
| MCP memory | memory.json (JSONL) | One note per entity; relations → [[wikilinks]] |
| Claude projects | projects.json or projects/<uuid>.json | A project folder: documents + instructions |
| Claude memory | memories.json | One note per account memory block |
| Claude design chats | design_chats/<uuid>.json | One note per chat |
| Markdown / text | .md, .txt | One note, the file body = the note body |
The format is determined by analyzing content (every service ships a file named conversations.json, so the name is unreliable). Empty messages and conversations without a single meaningful fragment don't create "orphan" notes — the number skipped is shown in the import summary. If the archive contains JSON that isn't recognized, it too lands in the summary as unsupported — data loss is always visible in the summary rather than happening silently.
How to import
Import lives on the Import tab in the space settings (/s/<space>/management/import). Each option is its own section:
- File — pick the export file (
conversations.json, the whole ZIP export, ormemory.json). A standalone.md/.txtfile is imported by drag-and-drop (see below), not through this dialog. - Skip existing notes — what to do on a repeat import (see below).
- Memory entries — where to put memory entities (see below).
A long import runs as a durable job: a progress bar with a live counter of notes written, the current phase, and a Cancel button. You can leave the tab and come back — the import keeps running in the background, and when you return you'll see the progress again or the final summary.
The separate upload dialog is optional: drop a .md or .txt file straight into the app window and it becomes a note. Dropping onto a folder in the tree files the note there; dropping into the content area files it into the folder of the open note, or into the root. It's the same import pipeline, just a second entry point into it.
The "Skip existing notes" option
A note's file name is deterministic and tied to the source's identity. Because of that, re-importing the same export overwrites the same files rather than breeding duplicates — and fifty "Untitled" conversations don't collide with one another.
- Off (the default for re-uploads — upsert) — existing notes are overwritten by path, idempotently.
- On — notes whose path already exists are skipped. This is the "I re-imported an updated history — don't clobber what I already fixed by hand" case.
The "Memory entries" option
Entities from memory.json can be routed to one of three places:
- folder — visible user-facing notes under the import's root folder.
- space — into the space's hidden agent-memory mount (
.notarium/memory): these entries don't show up in the tree, the Feed, or search, but the agent can reach them viarecall. - skip — don't import the memory at all.
The space option puts entries into the agent memory of that specific space. There's no separate UI browser for this memory inside the space — the agent sees it through recall. This is the memory of one specific space, not the global personal memory domain (which opens as the Memory lens in the explorer tree) — this import doesn't write there.
How the data is laid out
Import creates a predictable folder tree inside the chosen root:
conversations/claude/ — Claude conversations
conversations/chatgpt/ — ChatGPT conversations
projects/<project>/ — Claude projects (+ docs/, prompt-template.md)
memory/claude/ — Claude account memory
memory/<entity-type>/ — entities from memory.json
design-chats/<project>/ — Claude design chats
Dates are preserved as data
A naive import would date the whole history as "today," and the Feed would pile hundreds of conversations into one heap. Notarium instead carries the creation date through as data: every note gets a created: in its frontmatter set to when the conversation actually happened. The Feed spreads imported history across its real days, and the export → import loop preserves dates — nothing is lost in the move.
The "Created" field can also be edited by hand from the editor (a note's metadata) — for example, during a migration or when correcting a note's date. The last-modified time (modified) always reflects when the file was actually edited and can't be changed.
How it works under the hood
Import is built to withstand both gigabyte-scale archives and a dropped connection:
- Streaming processing. The upload is streamed to disk, the ZIP is unpacked one member at a time, and the JSON array of conversations is parsed element by element — one conversation at a time. Peak memory doesn't depend on the size of the archive, so a 600 MB export won't crash the server.
- Durable job. When the host has a metadata database (the norm for self-host), import defaults to a durable job: the upload is saved to staging, and a background worker writes the notes outside the request. A closed tab, a lost connection, even a server restart — none of them cost you any progress.
- Cooperativeness. A bulk import doesn't monopolize the server: writing yields to interactive requests, and background indexing pauses for the duration of the stream and catches up afterward. Search and navigation stay responsive even while thousands of notes are being written.
flowchart LR
src([Archive / file]) -->|upload| stage[Staging on disk]
stage -->|import job| worker[Background worker]
worker -->|write-path| notes[(Markdown notes)]
worker -.->|progress| ui([Import tab])
On a host with no metadata database (and it's absent only in AUTH_MODE=none mode) there's no job layer: import runs the synchronous streaming path within a single request, with the same core and live counter. Progress and the summary look the same; the only difference is that the import won't survive a server restart.
Boundaries
- Cancel, but not pause. A job can be canceled (cooperatively), but not paused and resumed.
- The indicator is indeterminate. The number of notes in an archive isn't known in advance, so progress shows the phase and a live counter of notes written, not a percentage and ETA.
- An interrupted upload starts over. Durability kicks in once the bytes have arrived: if the upload of a large archive is cut off, it starts over.
- Binary attachments aren't imported. Text from attachments is embedded in the note body, but the binary files themselves aren't (same as in export).
- Import lands in the space root. The import dialog doesn't let you pick a target folder — notes go to the root; with drag-and-drop the root is determined by the drop zone.
Next
- Export — take a space or folder back out as a Markdown archive.
- Agents and MCP — how an agent works with imported memory via
recall.