Local-first outliner. Markdown is the source of truth. Sync that doesn't corrupt your tree when two devices edit offline.
Inspired by Roam Research and Logseq. Picks what they got right (graph, backlinks, daily journal, block-level thinking) and fixes the part they didn't.
This is the differentiator. Everything else builds on it.
Roam keeps your notes on their servers; if two devices edit while
offline, the later write silently wins. Logseq scatters id:: UUIDs
through your .md so its rsync-flavoured "sync" has something to
match on; concurrent moves still lose data. git-as-sync produces
conflict markers across nested bullets every time.
outl uses the Kleppmann et al. 2022 tree CRDT — the
same family of algorithm that backs Automerge and Y.js, adapted
specifically for trees. Two devices that edit a workspace offline
and then sync produce exactly the same tree, with no data
loss, without a server, and without polluting your
.md. The IDs CRDTs need to operate live in a separate sidecar
file (.foo.outl), so the markdown you see is the markdown you
wrote — no id:: lines, no UUIDs, no HTML comments.
Five formal guarantees, each backed by a test in
crates/outl-core/tests/:
- Strong eventual consistency — same set of ops → same tree, any order.
- Commutative after reordering — late arrivals don't break the result.
- Idempotent — applying an op twice is the same as once.
- Tree invariant always holds — no node ever has two parents, no cycles.
- No silent loss — every op stays in the log, even ones turned into no-ops by cycle detection.
→ Sync, done right walks through why Roam, Logseq and Git fail, then the algorithm step by step.
→ Tree CRDT walkthrough is the algorithm with code.
P2P sync transport (phase 2) is on the roadmap; the algorithm and the op-log infrastructure are already in.
- TUI — journal-first, vim-style keys, slash commands (
/), fuzzy switcher (Ctrl+P), workspace search, multi-line blocks, fenced code blocks, themes, hot-reload on external.mdedits. - Markdown clean as you wrote it —
title::,icon::,tags::properties live in plainkey:: valuelines at the top; outline is standard CommonMark bullets. No metadata smuggled in. - Page icons —
icon:: 🚀on a page surfaces everywhere it's referenced (header, switcher, backlinks panel,[[ref]]inline). - Code blocks that run —
```lisp / ```js / ```python / ```lua / ```rust, the result lands as a> **result:**subblock under the source. Re-runs are idempotent. Setauto-run::on a block and it re-runs whenever you open the page (cache-aware by source hash). Powered byoutl-exec— language registry is plugin-shaped, more languages drop in as 80-line adapters. - Importers —
outl import logseqandoutl import roamstripid::lines, resolve((uid))block refs, slugify filenames, seed sidecars. - Bench harness —
cargo bench -p outl-mdmeasures parse + index over synthetic workspaces from 15 files up to 10.500. CI runs the smaller tiers on every PR; the 10k-file tier on a weekly cron.
git clone https://github.com/avelino/outl.git && cd outl
cargo build --release
./target/release/outl init ~/notes
./target/release/outl --path ~/notesoutl (no subcommand) opens the TUI on the workspace and lands on
today's journal. Press ? for keymap, : for the command palette,
Ctrl+P to fuzzy-jump to any page.
outl import logseq ~/path/to/logseq-graph ~/notes
outl import roam ~/Downloads/backup.json ~/notesThe importer strips id:: lines, resolves ((uid)) block refs to
page links, slugifies filenames, and seeds the sidecars. Anything it
can't resolve stays as ((unresolved:UID)) for manual triage.
0.1.0 — single-device editor, daily-driver-ready. Algorithm and
op log infrastructure for sync are in place; the network transport
(phase 2) is the next major piece. Desktop (Tauri, phase 5) and
mobile (uniffi, phase 6) reuse the same outl-core + outl-md
crates that the TUI already drives — see
the roadmap.
Want to actually learn how this works?
→ docs.outl.app — full GitBook, with the sync algorithm walked through step by step, the TUI manual, theming, and contributing notes.
MIT.