format spec

the format,
all of it.

Standard CommonMark, plus six conventions for properties, refs, tags and runnable code. That's it. The .md you read is the .md outl wrote.

anatomy of a page

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.

pages/launch.md
title:: Launch icon:: 🚀 tags:: launch, q2-2026 - ship 0.1.0 #launch - draft [[release notes]] - post on [[avelino.run]] - code block runs: ```python print(2 + 2) ``` > result: 4
.launch.outl json sidecar
{ "version": 1, "blocks": { "01HF...A1": { "text": "ship 0.1.0", "parent": "ROOT", "props": { "status": "done" } }, "01HF...A2": { "text": "draft [[release notes]]", "parent": "01HF...A1" } }, "tags": ["launch", "q2-2026"] }

Lose the sidecar? outl doctor regenerates it from the op log. Lose the .md? The op log still has every block.

01 · properties

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 (phase 3) treat them as columns. Anything key:: value indexes.

02 · references

[[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. No nesting today (#a/b is two tags). shipped #launch
[label](url) Standard markdown link. Untouched. [paper](https://...)
03 · code blocks

fences run.

Standard markdown fenced code blocks. Five languages are recognized as runtime — the rest stay as syntax-highlighted prose. Full walkthrough at /code.

```python
RustPython
```lisp
Steel (Scheme R5RS-ish)
```js
Boa (ES2015+)
```lua
mlua 5.4
```rust
rustc → wasm32-wasip1 → wasmtime

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.

on disk
- the answer ```python print(6 * 7) ``` > result: 42 source-hash:: a91f2c3
04 · what outl deliberately doesn't do

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 plain key:: value lines 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 .md in cat. Nothing reads like an artifact.
05 · external edits

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. 1.
    Exact text match.

    If a block's text is unchanged, its ID stays. Most edits move blocks around, not text.

  2. 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. 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.