the format,
all of it.
Standard CommonMark, plus
a small set of conventions for properties, refs, tags, tasks and runnable code.
That's it. The .md you read is the .md outl wrote.
one file. two layers.
Every page is one .md + one .outl sidecar. The .md is what humans (and other tools) read. The sidecar holds CRDT IDs and metadata that doesn't belong inline.
Lose the sidecar? outl doctor regenerates it from the op log. Lose the .md? The op log still has every block.
key:: value.
Every property is a plain markdown line: key:: value. Two colons. Space. Value. That's the entire grammar.
Page-level properties sit at the top of the file before any bullet. Block-level properties sit on the line below their block. outl recognizes a handful of reserved keys; everything else is yours.
| key | scope | what it does | example |
|---|---|---|---|
title:: | page | Display name. Slug is the filename. | title:: My Launch Plan |
icon:: | page | Single emoji. Surfaces in header, switcher, backlinks, autocomplete, inline [[refs]]. | icon:: 🚀 |
tags:: | page | Comma-separated tag list applied to the whole page. | tags:: launch, q2-2026 |
alias:: | page | Alternate names for the page. [[Alternate Name]] resolves here. | alias:: launch, q2 launch |
auto-run:: | block | Code blocks under this property re-run on page open (cache-aware). | auto-run:: on |
source-hash:: | result | Written by outl onto > result: subblocks. SHA-256 of source. Don't edit by hand. | source-hash:: a91f2c3 |
priority:: | block | Custom property. Any key:: value pair works. Used by queries. | priority:: p1 |
Custom keys are first-class. Queries (planned) treat them as columns. Anything key:: value indexes.
[[pages]], ((blocks)), #tags.
Three reference primitives. All of them stay in the markdown — nothing is rewritten with internal IDs visible to you.
| syntax | what it does | in context |
|---|---|---|
[[Page Name]] | Wiki-link to another page. Resolved via slug. If page doesn't exist, the link is clickable and creates it. | [[Launch Plan]] is on track |
[[Alternate Name]] | Resolved via alias:: on the target page. | [[q2 launch]] (alias) |
((block-id)) | Block reference. Renders the referenced block inline. ID maps to sidecar entry. | ((6624a82c)) |
#tag | Inline tag. Indexed and queryable. | shipped #launch |
#a/b/c | Namespaced tag. A page per level, and the parent lists and collects its children. | ran on #os/linux/debian |
[label](url) | Standard markdown link. Untouched. | [paper](https://...) |
a slash makes a hierarchy
#os/linux/debian parses as one tag, resolves to one page, and keeps its name verbatim — that much was always true. What the slash gets you now is everything a hierarchy implies. The os page lists every descendant below its backlinks, at every level, indented by depth. And a block tagged #os/linux is a backlink of os too, so the parent collects what its children collect.
three states,
two spellings.
A task is a word at the front of a block. There used to be two of them, and no way to say a task was underway — which meant everybody invented their own convention with a tag or a property, and none of it reached a status: query or the progress counter. DOING is that third state.
| word form | checkbox form | state | meaning |
|---|---|---|---|
TODO | - [ ] | TODO | on the list, not started |
DOING | - [/] | DOING | underway |
DONE | - [x] | DONE | finished |
The CommonMark checkbox is the second spelling, and a block written that way is a task everywhere a TODO block is one: it draws a checkbox, answers status: queries, counts in the progress chip, and toggles. It used to be inert prose. The toggle chord walks one stop per press — none → TODO → DOING → DONE → none — and the first toggle of a checkbox rewrites it into the word form, where it stays.
The trailing space is what separates a task from prose, which is why DOINGs are piling up is a sentence and not a started task. And the progress counter puts DOING on the total side, never the done side — a started task is unfinished work.
fences run.
Standard markdown fenced code blocks. Five languages are recognized as runtime — the rest stay as syntax-highlighted prose. Full walkthrough at /code.
the result subblock
A blockquote starting with › result: immediately under a code fence is the output. outl owns this subblock — re-running rewrites it in place. Don't edit by hand; your edits get overwritten.
markdown stays
markdown.
- ✕ No
id::lines. Block IDs never appear in your markdown. They live in the sidecar. - ✕ No YAML frontmatter delimiters. No
---separators at the top. Properties are plainkey:: valuelines you can read in any markdown tool. - ✕ No HTML comments smuggling metadata. No
<!-- collapsed:: true -->. State lives in the sidecar. - ✕ No custom non-CommonMark syntax. If you copy a block out of outl into pandoc, hugo, or your editor's preview, it renders as you'd expect.
The test: delete outl tomorrow. Open every.mdincat. Nothing reads like an artifact.
edit in vim.
outl catches up.
When you save a .md externally, outl runs a 3-level matching algorithm to figure out which block in your file maps to which ID in the sidecar:
- 1. Exact text match.
If a block's text is unchanged, its ID stays. Most edits move blocks around, not text.
- 2. Structural match.
Parent + sibling position. If text changed but the block is in the same place, it's the same block — text is just an edit.
- 3. Fuzzy text similarity.
Levenshtein distance over candidates. If structure changed too, the most similar surviving block keeps the ID. Ambiguous matches surface in
outl reconcile.
Duplicating a block in VS Code gives the duplicate a fresh ID (no collision). Renaming a page updates the slug; title:: stays human. Full algorithm in docs/markdown-format.
one format,
no surprises.
The full reference is open source on github.