An Addressable, Lossless, git-friendly plain text format wants to bridge Logseq DB graphs and Logseq OG

The split announcement lists Markdown support in the DB version as open research — “treating each Markdown file as a single block” and “enabling reliable two-way sync with Markdown files” are named as the approaches being explored.

DB graphs solved real problems, but they left one thing behind: a text form of
your graph. Markdown export is readable but lossy; EDN export is lossless but
not something you’d read or edit. So today a DB graph can’t really be
git-versioned, diffed, reviewed, or safely edited by anything outside the app.

We’ve got a full, verified round trip working by introducing a middle ground with
GEML (an id-native, human-readable text
format), using only the official @logseq/cli tooling:

[Logseq DB]
   └─ export-edn → readable .geml files (one per page)
        └─ edit any block externally (git / scripts / agents)
             └─ import-edn → [Logseq DB]

Coming from OG? The tree is laid out like your old vault — pages/<name>.geml, journals as journals/2025_02_20.geml, no database-dump filenames.

:white_check_mark: Passes logseq validate. Edits merge back in place by UUID — zero
duplicated blocks. A real exported block:

=== text {#aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee level=1}
hola, Logseq feels good with GEML!
===

Why this maps so cleanly

In Markdown, block identity (id::) is a fragile convention. In GEML it is
grammar, and Logseq DB’s model lines up almost 1:1:

Logseq DB GEML plain text Status
Block UUID {#uuid} on the block :white_check_mark: today — identity survives any text edit
Outline tree flat blocks + level=N :white_check_mark: today
Typed properties carried losslessly in a companion metadata block :white_check_mark: today · readable {key=val} attrs are the next step
Block refs ((uuid)) [[#uuid]] checked references next step — one character away, then broken refs fail the build

What this unlocks

  • :herb: Real git workflows: clean commits, readable diffs, version control
    for your DB graph.
  • :robot: Agent & CLI tooling: read and write single blocks by UUID, app closed.
  • :locked: Data ownership: a plain-text escape hatch that stays yours.

:link: Code & step-by-step demo: GitHub branch

Finally, we want to here real voice from you:

Would a plugin that does this be useful to you?

  • Yes
  • Maybe — depends on the features below
  • No
0 voters

Which features would matter to you? (pick any)

  • :repeat_button: Continuous two-way sync (not just one-shot export/import)
  • :package: Plain-text backup & data ownership
  • :robot: LLM / agent automation
  • :wrench: Bulk text refactoring
  • :herb: Git versioning & diffs
0 voters

Comments welcome — especially if you voted Maybe or No: what is missing?

This looks really promising! I’ve checked Git versioning and continuous sync only. IMHO, it might be best to keep the plugin focused on these aspects and avoid adding too many extra features. Simplicity is key! Keep a clear goal in mind (solving a specific problem). Don’t create a monster that does everything. Other features are interesting too, but not in the same plugin. Fingers crossed!

Thank you — this is exactly the kind of signal we hoped the poll would surface, and we agree completely.

The good news: the architecture already enforces the focus you’re asking for. The plugin’s only job is the bridge — keeping your DB graph and a folder of plain-text files in sync, faithfully, both ways. Everything else on that feature list (agents, bulk refactoring) was never going to be plugin code: once your graph is ordinary text files, git and any external tool work on them without the plugin knowing or caring.

So “git versioning + continuous sync” isn’t just the focused scope — it’s the entire plugin. The rest is what other tools do with the files, outside it. Fingers crossed indeed :crossed_fingers:

“Do one thing and do it well” is exactly our philosophy here. The goal of this plugin is strictly a **lean, reliable sync bridge**:

- Keep your DB graph continuously synced to clean, readable plain-text files on disk.

- Let standard `git` do the rest (clean diffs, history, branching).

The beauty of having real plain-text files on disk is that other use cases (like LLM agents or bulk scripts) can live entirely **outside** Logseq, using standard CLI tools without bloating the plugin itself. No monster plugins here!

The marketplace PR (logseq/marketplace#893) is still in review, so until it
lands:

  1. Logseq → Settings → Advanced → Developer mode
  2. Download logseq-plugin-sync-vault-with-geml-v2.0.8.zip from
    Release Sync Vault with GEML v2.0.8 · geml-spec/logseq-plugin-sync-vault-with-geml · GitHub
    and unzip it
  3. Plugins → “…” → Load unpacked plugin → pick the unzipped folder
    (the one with package.json in it)
  4. Pin to show sync icon in tool bar
  5. Settings → Plugins → Sync Vault with GEML → set the Vault folder.
    Do this BEFORE step 6: the watcher has no default and refuses to guess one.
    (Or skip it and pass the folder instead: logseq-sync ~/logseq-vault)
  6. npm i -g @geml/logseq-sync then run: logseq-sync

Stuck? logseq-sync doctor prints what it can see — graph, CLI, vault folder,
git — and names the missing piece.

1 Like

The thing that got me was identity as grammar, not as a convention the editor might keep.

I have lost block ids in Markdown enough times that I stopped treating id:: as real. Git on a file that reshuffles every indent is also not version control. It is noise. EDN

I can round-trip and never actually open, so it does not count.

A folder of files that merge back by UUID is the missing piece. I agree with J-D. The plugin should stay the bridge. Once the graph is files, git and scripts do not need to live inside Logseq. That is the point of getting them out.

I have not run the round trip on a live graph yet. Two things I would want before I trust it: typed properties readable on the same block, not only in a companion metadata blob, and two-way sync that does not duplicate a block when both sides moved. You said UUID merge already handles the second. The first still looks like the next step.

You are right that it is still a companion block, and I do not think that part can go: flattening EDN sets and keywords into fence attributes would lose exactly what makes them typed. What I could fix was everything that made it feel like a blob.

In [2.3.0](https://github.com/geml-spec/logseq-plugin-sync-vault-with-geml/releases/download/v2.3.0/logseq-plugin-sync-vault-with-geml-v2.3.0.zip) it sits directly under its block, carries Logseq’s own EDN (:build/properties, keywords intact), and is addressable — one property, not the file:

printf 'done' | geml set pages/foo.geml \

'#meta-<uuid>[":build/properties"][":user.property/status"]' --in -

One value changes, every other byte stays. Maps are written one entry per line, so that is a one-line diff. And the uuid is stored once now — it used to be in both the address and the blob, with the tool believing the blob, so hand-editing the address was silently ignored.

Your second condition, half: imports land by uuid so nothing duplicates, but if both sides moved the same block it is held as a conflict, not auto-merged.

The open question is layout, because it collides with the “git and scripts” point:

Logseq 2.x DB graph  ⇄  .logseq-sync-vault-with-geml/*.geml    two-way, LOSSLESS

→ pages/*.md journals/*.md one-way, LOSSY

The thing right now: the visible root is the lossy Markdown copy, purely so the folder opens in Logseq OG. But Markdown edits never sync back. If you run git or scripts on what you see, you are touching the wrong tree — the lossless truth lives inside that hidden dot-directory.

Should GEML be the root (the files you actually see, commit, and script against), with Markdown moved to an export subfolder or flag — or is opening the root directly in Logseq OG still worth that inversion?