Verified against MindNode 2026.4.4 (com.ideasoncanvas.mindnode) on macOS.
No AppleScript dictionary exists; this file documents the local library format
the read pipeline relies on, plus the app's other automation surfaces.
~/Library/Containers/com.ideasoncanvas.mindnode/Data/Library/Application Support/
MindNode/<environment>/MindNode Library.mindnodelibrary/
Content.sqlite3 # GRDB database (WAL mode)
Assets/ # base snapshots + preview JPEGs
Caches/
<environment> is production-v1_0 today; scan for any directory containing
a .mindnodelibrary with Content.sqlite3.
document— one row per mind map:documentID(UUID),title,folderID,trashDate(NULL = not trashed),localModificationDate,baseSnapshotID.serializedDatais NULL in practice; content lives elsewhere. CloudKit sync bookkeeping fills the remaining columns.operationBatch— the CRDT edit stream:documentID,creationHLCTimestampString(orderable),serializedData(envelope, see below),operationCount,peerID.snapshot— snapshot metadata rows; the snapshot payload itself is the fileAssets/<snapshotID>(today snapshotID == documentID).operation— empty in practice (ops arrive pre-batched).
Document state = base snapshot ⊕ operation batches in HLC order.
Open the DB read-only, always — MindNode owns it and syncs it to CloudKit; a stray write could corrupt sync state.
Every stored blob (base snapshot file, operationBatch.serializedData) is a tiny protobuf envelope:
| field | type | meaning |
|---|---|---|
| 12345 | varint | format version (10 as of 2026.4.4) |
| 678910 | bytes | payload |
(Field numbers 12345/678910 look like developer humor; they're stable.)
If the payload starts with bv4 it is an Apple Compression-framework LZ4
frame: blocks of "bv41" u32le_decodedSize u32le_encodedSize <lz4 block> or
"bv4-" u32le_size <raw> terminated by "bv4$". Small payloads (the 324-byte
template snapshot) skip compression entirely.
Top level:
f1— document containerf1— doc-level record- repeated
f2— node records:{f1: nodeID(16B), f2: body}where body hasf7= ordinal,f9= style (RGBA doubles),f11= title (f11.f1.f4.f1= text run string, repeated),f12/f13= misc f3— relations: repeatedf2records{f1: childID, f2: parentID, f4: {f1: {f1: peerID, f2: sortKey}}}. The root node's parent is the documentID itself. Sort keys are spaced (200, 400, 600 …) for fractional insertion.f5/f6— theme;f6.f2nests another LZ4 frame containing JSON
f2— checksum/opaquef3— HLC clock state
A fresh document's snapshot is a 324-byte template with a single node titled "Mind Map" (node UUID identical across documents). Markdown/OPML imports produce a full snapshot with all nodes and relations and zero op batches.
Top level: f1 header (documentID), repeated f2 op records:
- op record:
{f1: opID(16B), f2: <envelope again>}; the inner payload hasf1opID,f2meta (f1= HLC timestamp fixed64,f3= peerID),f3= the op body, keyed by op type:f3.f1…structural group — contains creation records{f1: newNodeID, f2: parentNodeID, f3: origin}nested a few levels downf3.f16— text edit:f1= nodeID, subtree holds inserted text runs asf4strings (CRDT position info sits in sibling varints — not yet mapped, so heavy mid-string edits can reassemble slightly out of order)f3.f18— style ops;f3.f8/f3.f10— geometry/selection-ish; ignored
Deletions are not yet mapped (deleted nodes may linger in output as
(untitled)).
document.folderID is NULL even for documents that sit in a folder — do
not read it. The folder tree and its membership live in the library table's
serializedData, which is the same envelope + LZ4 + CRDT op stream as a
document. Its ops carry:
- folder creation
{f1: folderID, f2: parentID}(parent = libraryID for a top-level folder, another folderID when nested), - folder rename
{f1: folderID, f2: "<name>"}— folders are born as "My Folder" and renamed, so take the last rename op per folderID, - document membership
{f1: documentID, f2: folderID}.
Replaying those gives paths like CoreSpeed / Work Log. Nothing in the write
path can target a folder yet; MoveDocumentsIntent is the App Intents route.
MindNode caches its own renders next to the snapshots:
Assets/<documentID>_{fullSize|thumbnail}_{light|dark}_<hash>.jpeg — a free,
pixel-perfect ground truth for any document that has been opened once.
- File import:
open -a MindNode <file.md>silently imports into the library as a new document (first#heading = central node). This is the currentcreate_mindmapmechanism. Verified behavior:- Markdown is the only format that works this way. MindNode registers
as a Viewer for OPML, FreeMind, TaskPaper and friends, but
open-ing those produces no document at all — silently ignored. - Nesting needs 4 spaces or a tab per level. Two-space indentation, the
common Markdown convention, is flattened into siblings.
##headings also nest. (normalizeListIndentinsrc/actions.tsre-emits any self-consistent indentation at 4 spaces.) - Title collisions are auto-renamed by MindNode: importing "Aug 19" while an "Aug 19" exists yielded a document titled "Aug 2". The central node keeps the requested text.
- The document lands at the library root; the importer offers no way to
target a folder (folders live in the
libraryblob's own CRDT op stream — see below).
- Markdown is the only format that works this way. MindNode registers
as a Viewer for OPML, FreeMind, TaskPaper and friends, but
- URL scheme:
mindnode://documents/<documentID>/content/,mindnode://documents/by-name/<name>/content/,mindnode-next://newDocument?type=mindMap|outline. - App Intents (Shortcuts, 20 of them): CreateDocument, CreateNode,
EditNode, DeleteNode, DeleteDocument, ExportDocument, ImportDocument,
GetCurrentDocument, OpenDocument, OpenRecentDocument, RenameDocument,
MoveDocuments, CreateFolder, MoveFolder, ShareViaWeb … Only reachable from
the Shortcuts app /
shortcuts run <wrapper>, so using them requires installing wrapper shortcuts once (planned phase 2 for node-level edits and lossless export).
tools/spelunk.py <blob-file> pretty-prints any envelope/snapshot/op-batch
blob as a raw protobuf tree (handles the envelope and LZ4 framing).