Obsidian to Logseq DB import

When the DB version will be fully released and operational, will there be at some point some onboarding wizard from other PKMS tools, namely Obsidian ? Given that both products will have similar features (properties), differences (outliner vs paragraphs), tags… the conversion won’t be so trivial and should probably be offering some conversions options/wizard mode guiding personal choices.

Would that onboarding process be something that could be offered by Logseq app with extension points so people can contribute different “onboarding / conversion plugin” extensions for different PKMS ? Or it is something that will be completely out of scope of Logseq and let it all in the hand of plugin/external apps writers ?

Hi @Tobei

Did you ever find a way to do this easily? I am currently trying to import my notes across to logseq dn at the moment.

Would be awesome for migration from Obsidian, Microsoft OneNote, Evernote, Notion, Apple Notes, Google Keep. And export to them too.

i just did this a few hours ago with a lot of help from claude. you can use claude, codex or any other llm for help if you trust them on your files. here’s what i did to make it work…

# Runbook: Obsidian vault → Logseq 2 (DB version)

Worked out in one long session (2026-10-09). The converter lives in `~/tools/obs2logseq/`

(`convert.py`, `test_convert*.py`, `verify.py`, `compare.py`). Route that worked:

1. Convert the vault to a Logseq **file graph** with our own tool.

2. Import it with Logseq’s **Import → File to DB graph** (options: “Import all tags” on,

“Remove inline tags” off, “Extract inline code snippets” off).

3. Verify with the **Logseq CLI** (`~/.local/bin/logseq`, read-only queries).

4. Make an **EDN backup**, then clean up.

The CLI cannot import a file graph. Every import is a manual step in the app.

An import of a few thousand pages took about 2 minutes.

## Rules

- The vault is read-only. Never write to it. Keep a pristine copy (`cp -c -R` is an APFS clone).

- One output folder per version. Delete the previous one when the next is verified.

- Never overwrite: the converter refuses when the output folder exists.

- Do not run CLI commands against a graph while the app is importing it.

- Do not run `logseq graph validate` (issue #1067: it can change blocks).

- `logseq graph remove` only **unlinks** the graph (moves it to `~/logseq/graphs/Unlinked graphs`).

The app keeps a stale entry in its list until it is restarted.

- Python runs through `uv run --with pyyaml python -I script.py` (needs `dangerouslyDisableSandbox`

in Claude Code; bare `python3` may be blocked by a hook).

## Prompts, in order

### 1. Plan, read-only

```

I want to move from Obsidian back to Logseq 2 (the DB version). My vault is at .

Treat it as read-only: never write to it. Look at the files first: folders, frontmatter,

tasks, queries, embeds, assets, journals. Then write a plan with steps and tests.

Do not convert anything yet.

```

### 2. Decisions, so there is no rework

```

Decisions for the conversion:

- Task markers back: TODO/DONE/LATER/NOW/CANCELED. Properties as `key:: value`.

- Remove SCHEDULED, DEADLINE, task dates (:white_check_mark: :date:), :LOGBOOK:, State/Clock lines.

- Remove ballast metadata: old flashcard card-* values, junk left by a highlights sync (a `link::`

property with the value `null`, an `on::` date of 1970), empty properties, the migration date `created::`.

- Obsidian-only notes in subfolders become normal (namespace) pages.

- Obsidian Bases embeds are dropped.

- Auto-link plain mentions of people who have their own person page.

- Tags in the middle of a sentence become page links; tags at the end of a block stay tags.

- Dates in links get the journal title format (full month names and missing ordinals are fixed).

- If a query or embed breaks the importer, put it in a code block. Lose no information.

```

### 3. Converter with tests

```

Write a converter from my vault to a Logseq file graph in a NEW folder .

Rules: never overwrite; refuse if the folder exists. Write tests on real fixtures from my vault

for every rule. Print stats and a log. After each run, compare words between vault and output

(word bag per file) and show only the words that disappeared. Only intended words may disappear.

```

### 4. Small probe graphs

```

Make a probe graph (folder ) with about 20 different pages: person pages, meeting notes,

journals, highlight imports, PDF annotation pages (hls__), queries, tables, tasks, namespace pages,

pages with embeds and block references. I will import it with Import → File to DB graph and paste the console

and the toast messages. Do not use the Logseq CLI during my import.

```

### 5. Full import and verification

```

I imported the output as graph . Check it with the Logseq CLI (read-only queries only):

1. Every vault file → page in the DB, with block count. List empty and missing pages.

Filter known artefacts: namespace pages are stored under their last name part, hls__ pages

are stored under the PDF asset.

2. Every block of every converted file → exact match in the DB (resolve the uuid page and block references first).

3. Task counts per status, block ids, property keys, asset references, search index size.

4. Run a few search queries (person names, a phrase). Report only real differences.

Do NOT run `graph validate`: issue #1067 says it can change blocks.

```

### 6. Find the cause of a page that will not import

```

Page is missing or empty in the DB after import. Split it into one page per

top-level block (K00, K01, …) in a new probe folder and give me the folder. I import it once.

Then use the CLI to see which parts are missing and narrow it down until you find the line.

```

### 7. Check Logseq’s known issues

```

Read the open issues of the Logseq `db-test` repository on GitHub with `gh`. List the ones that

concern file-to-DB import, tags/properties/classes, EDN, search, assets and tasks.

Check each one against my files and my graph. Say which ones affect me and which do not.

```

### 8. Review by agents

```

Take a random sample of pages per kind (person pages, meeting notes, journals per period, plain pages,

highlight imports, PDF annotation pages, queries, tasks, namespace pages). Per page make a snapshot file with

A = original, B = converted markdown, C = how the DB shows it (`logseq show`).

Start 5 haiku agents (read-only). Tell them first what was done on purpose and the known false

alarms: `show` prints refs and images as page names in brackets; `- ## x` is a normal heading; link case is

normalised; queries show as `- #Query`; embeds are not shown; namespace and hls__ pages show

as “not in database”; end-of-block #tags are moved out of the text.

Verify each agent finding yourself against the files and the DB before changing anything.

```

### 9. Backup and clean-up

```

Export the good graph with `logseq graph export --type edn --file ` (new file only).

Then list the old output folders with their sizes. Show me the list, wait for my yes,

and then delete them. Keep the backup and the pristine copy of the vault.

```

### 10. Runbook and git

```

Write RUNBOOK.md in the converter folder: the steps, the decisions, known importer problems,

and the false-alarm list. Put the converter, tests and runbook in git in small commits.

```

## Known importer problems and the fix we used

| Symptom | Cause | Fix in the converter |

|—|—|—|

| Page missing, import “valid” | Empty embed (an `{{embed}}` with an empty page name; “No uuid found for page name ‘’”) | Put it in inline code |

| Page missing, “No ident found for :” | Orphan `query-sort-by::`, `query-table::` on a block that is now code | Move the options to one plain text line |

| Page missing or empty, `SCHEDULED` error | `SCHEDULED:`/`DEADLINE:` lines | Drop them (and the task marker, by decision) |

| Import stops on advanced query | `#+BEGIN_QUERY` | Put in a ```` ```clojure ```` block |

| `#tag` cut out of a sentence | Importer makes inline tags into block tags (“Talked to #name about it” → “Talked to about it”) | Tag in running text becomes a page link; tags at the end of a line stay |

| Validation error on a page | Property name equals a tag used as class (`status::` next to `#Status`) | Rename the property (add a suffix) |

| Date property or date link ignored | Date not in journal format (full month name, missing ordinal) | Normalise to the journal title format |

| Property value ignored | Value does not fit the type of the other values of that key | Keep it as a plain text line |

| Two files with one name (one empty file in the root, one with content in a subfolder) | Name collision | The file with content wins |

| 1 asset ENOENT in log | File is really missing in the vault | Nothing to do |

| Flashcard `card-*` values not imported | Logseq 2 has its own scheduling | Drop them (by decision) |

| Image alt text lost | Issue #1081 | None |

Other things to know:

- Namespace pages (`A___B.md`) are stored in the DB under their last name part.

- `hls__*` PDF annotation pages are not pages; their text sits under the PDF asset.

- A tag with the name of a built-in property (`#scheduled`) becomes a normal user class; it did not fail here (issue #1009).

- After an EDN restore: run “Rebuild search index” (issue #1385), restore only into a new empty graph, once.

- If the search finds nothing: command palette → “Rebuild search index”. Check the size of

`~/logseq/graphs//search/db.sqlite` (tens of MB for a few thousand pages; under 100 KB means not built).

## Verification scripts (rebuild if missing)

- `verify.py `: word bag per file, lists words that are in the vault but not in the output.

- `compare.py titles.json counts.json out.tsv`: vault file → page and block count in the DB.

Titles: `[:find [?t …] :where [?p :block/name] [?p :block/title ?t]]`.

Counts: `[:find ?t (count ?b) :where [?p :block/name] [?p :block/title ?t] [?b :block/page ?p]]`.

- Block match: dump `[:find ?pt ?u ?t :where [?b :block/title ?t] [?b :block/uuid ?u] [?b :block/page ?p] [?p :block/title ?pt]]`,

resolve the uuid page references and `((uuid))` block references via the page and block uuid maps, normalise (lowercase, words only, no tags/images), compare.

- Property values live on separate nodes: query `[?b ?a ?ve] [(namespace ?a) ?ns] [(= ?ns “user.property”)] [?ve :block/title ?vt]`.

- Useful CLI: `logseq graph list|info|export`, `logseq list property`, `logseq query --query ‘’ -o json`,

`logseq show --page “”`, `logseq search page|block --content “”`, `logseq server list|stop`.

## False alarms from review agents

- `show` prints references and images as page names with or without brackets; assets appear under their file name plus a number suffix.

- `- ## Heading` is a normal heading block.

- Link case is normalised (a link in capitals shows in lower case).

- `{{query …}}` shows as `- #Query`; `{{embed …}}` is not shown by `show`.

- `alias::` is stored as an alias, not as a visible property.

- Namespace pages and `hls__*` pages show as “not in database” when looked up by their full name.

- A bare `DONE` block in the source stays a bare `DONE` task.

- Text glued together in `hls__*` pages comes from the PDF text layer, not from the conversion.

## What went well, what did not

Went well:

- Converter with tests on real fixtures; a word-bag comparison after every run.

- Vault never touched; pristine copy and EDN backups.

- Small probe graphs; splitting a page into top-level blocks found the cause of a silent failure.

- Reading the db-test issues and checking them against the data.

Did not go well (do differently):

- Block-by-block comparison against the DB came late. Do it right after the first import.

- Problems were found one by one through full imports. Use probe graphs per problem.

- First hypothesis for a failing page was wrong. Bisect first, guess later.

- Agent review round 1 was mostly false alarms. Give the false-alarm list first, verify every finding.

- Too many output folders (a dozen, several GB each). One per version, delete the old one.

- Decisions about ballast and Obsidian-only notes came late and cost extra imports. Settle them first (prompt 2).

- A CLI-owned db-worker can run next to the app’s worker. Stop CLI servers before an import.