Workspace Format¶
my-notes/
├── Work/
│ ├── .joplin-folder.json
│ ├── Kubernetes--17a35454.md
│ └── Operations--912f01ac.md
├── Personal/
│ ├── .joplin-folder.json
│ └── Plans--27b8f102.md
├── .joplin-sync/ # internal state — never edit, never commit
│ ├── state.sqlite3
│ ├── workspace.json
│ ├── lock
│ ├── journal/ backups/ conflicts/ quarantine/ resources/
└── .gitignore # generated; ignores .joplin-sync/
Managed note files¶
First line: a single-line HTML comment with compact, key-sorted JSON. Then exactly one blank line. Then the byte-exact Joplin Markdown body (line endings normalized to LF).
<!-- joplin-md-sync: {"id":"17a35454fbb34ee080e29fba9ee88730","schema":1,"tags":["homelab","kubernetes"],"title":"Kubernetes"} -->
The exact Joplin Markdown body begins here.
Rules:
- keys:
id(32-hex, optional — absent means new local note),schema(currently1),tags(lowercase, sorted),title. Unknown keys make the file invalid. -->inside values is emitted as-->so the comment cannot terminate early; JSON parsing restores it.- No volatile fields (timestamps) live in the header, so unchanged notes are byte-stable across pulls.
- The body is never reformatted; embedded HTML,
:/resourceand:/notelinks, trailing whitespace, and Unicode are preserved verbatim. - A file with no header inside a notebook directory is treated as a new
note (title = file name without
.md). A file with a malformed header isINVALID_LOCAL_FILEand blocks push for that file only.
Filenames¶
<sanitized-title>--<first 8 chars of id>.md. Sanitization handles
Windows-invalid characters, reserved device names (CON, NUL, ...),
trailing dots/spaces, length limits, and preserves Unicode. Names are
cosmetic: renaming a file changes nothing (pull normalizes it back);
identity is the header id.
Notebook directories¶
Each managed directory holds .joplin-folder.json:
- Folder identity =
id. The directory name derives fromtitleand is normalized on pull (sibling case-insensitive collisions get an--idsuffix). - Rename/move a notebook by editing
title/parent_idin this file (or do it in Joplin and pull). - A directory without the file is a candidate new notebook; push creates it in Joplin and writes the file.
- Notes must live inside notebook directories, never in the workspace root (Joplin has no root notes).
.joplin-sync/ internals¶
state.sqlite3— see ARCHITECTURE.md. Authoritative sync history; if it is lost, the metadata headers allow reconstruction: identical sides are re-adopted automatically, divergent ones become conflicts.workspace.json— mode, options (backup_retention), optionalbase_url. The token is never stored here.journal/<run-id>.json— plan + per-op status for each mutating run.backups/<run-id>/— copies of local files taken before overwrites.quarantine/<run-id>/— local files removed by--propagate-deletes.conflicts/<conflict-id>/— conflict bundles.resources/— downloads fromresources pull.