olai
A git-native outliner: memory for the AI era.
Outlines are .jsonl files, one line per node.
Longer content is Markdown files referenced from the outline. Everything
is tracked in git, and agents edit through the same operations layer as
the keyboard.
nix run github:juspay/olai -- web path/to/outlines
The file format
Each outline is a .jsonl file: one JSON object
per line, one node per line. Nodes have stable ids and parent pointers, so
line-based git merges are safe and moving a subtree changes one field.
See the format spec.
{"id":"kitchen","ord":"a","title":"Kitchen renovation"}
{"id":"demo","parent":"kitchen","ord":"a0",
"title":"Demolition","done":"2026-08-09T14:02:11-04:00"}
{"id":"order","parent":"kitchen","ord":"a1",
"title":"Order the new cabinets #blocking",
"doing":true,"date":"2026-08-07","after":["demo"],
"doc":"cabinets.md",
"desc":"Measure twice — the corner unit is the risky one."}
The outline above, as stored on disk.
$ git diff
-{"id":"order","parent":"kitchen","ord":"a1", … }
+{"id":"order","parent":"now","ord":"a1", … }
Moving a subtree changes the parent
field. Children reference order by id and are
unaffected.
Compared to Workflowy and Dynalist
olai keeps the outlining model those tools established — indent, move, mirror, check off. The difference is where the data lives and what can reach it.
| Workflowy, Dynalist | olai | |
|---|---|---|
| Storage | A hosted database | Plain files in a directory you choose |
| Sync | Their cloud | git, or anything that moves files |
| History | In-app version history | git log; every edit is a reviewable commit |
| Concurrent edits | Resolved by their server | Line-based git merges |
| Programmatic access | Vendor API, where one exists | Any agent over ACP or MCP; any text tool |
| Source | Proprietary | Open source (AGPL-3.0) |
Markdown documents
Notes are Markdown. Longer content goes in .md
files, referenced from the outline by the doc field. Each
document gets its own page and appears in the sidebar's file tree.
# Cabinet specs
The corner unit is the **risky one** — see
[the measurements](measurements.md).
- 600 mm base × 4
- corner carousel × 1
cabinets.md — a plain Markdown file next to the outline, editable with any editor.
Cabinet specs
The corner unit is the risky one — see the measurements.
- 600 mm base × 4
- corner carousel × 1
Daily notes
A Markdown file named for a date — 2026-08-12.md,
anywhere in the directory — is that day’s note. No setting says so, and it
can live wherever your vault already puts it. Open the day and the note is
on top, with everything dated that day underneath it. Relative links
between your Markdown files keep working from there.
Daily/
2026/
08/
2026-08-12.md ← the 12th
2026-08-10-recap.md ← not a daily note
Convention, not configuration: the basename has to be the date and nothing else.
2026-08-12
Fog on the water. Only the ferry has a deadline, so it went first.
Task status
A node can carry one of three marks: done,
doing, or todo. A node with no mark is not a task
and gets no checkbox.
1/3 is a display annotation. A parent's
status is never computed from its children.
● no mark → todo → doing → ● no mark
done is not a stop on the ring.
Two keys write all three. ⌘+Enter finishes
something, and takes that back: done is the mark with an
instant on it, which is what puts the work on that day’s page.
⌘+⇧+Enter walks the mark on, one step
round the answers a person gives about work they have not
finished. The last stop is an answer rather than a gap — a node
carrying no mark is not an unfinished one, and walking to it is how you
say a row was never a task.
Finishing something is a thing you mean, not a thing to do on the
way past, so nothing stamps a completion while you are walking round —
and the walk will not take finished work backwards. Press it on a
finished row and the write is refused, in the same words an agent gets:
nothing should decide on your behalf that finished work is not
finished. ⌘Enter takes the done off, and the walk
carries on from there.
Dates, and what is owed
A node can carry a date: the day it is
scheduled for. The mark says whether it is work. Read together they answer
a third question, and no field stores the answer — /agenda is
that reading, taken of every outline at once.
overdue
kitchen.jsonl
Kitchen renovation
today
kitchen.jsonl
Kitchen renovation
trip.jsonl
upcoming
overdue(n) ⇔ n carries todo or doing
∧ day(n.date) < today
Derived when the page is drawn, stored nowhere, and
spelled once — the agenda's first section and the tone of every date
badge are the same predicate. day() is the first ten
characters and the comparison is plain string order: dates are text.
Overdue is a definition, not a
flag. Nobody sets it and nothing stores it. done
extinguishes it by construction — finished work is late at nothing —
and a date with no mark is an occurrence: a delivery, a
birthday, a ferry. It draws no checkbox, its pill never takes the
attention tone, and when its day passes it simply leaves. A day going
by is not a failure of a bullet.
Three sections, because they are three pieces of news. Overdue is every slipped task in the directory, oldest first, grouped by the outline it lives in — the one answer no day page can give, because a slipped task is on a day nobody visits. Today is what today's day page holds, minus what is finished. Upcoming is the next seven days that have anything on them, each heading a link to that day's own page; days with nothing do not appear.
A task nobody dated is absent entirely: it has no when to be late against, and inventing one is what this format refuses to do. An empty agenda says Nothing is due. and offers nothing to press — every row on it lives somewhere else, and that is where it is changed.
A date is set on the row. Press the date beside a
title and the picker opens in place under it; a row with no date has no
pill to press, so its way in is the ••• menu —
Set date…, or Change date… on a row that has one. What
is written is the ten characters you picked, never a timestamp this app
invented on the way, because the control is the browser’s own
<input type="date"> and a date is text.
Empty the box and the button becomes Clear date — the ••• menu's own verb, and the same write.
Keyboard editing
Click a title to edit it in place. Each edit is one operation through the same layer the agent uses, and each lands as a git commit.
| Enter | commit, and open the next line |
| Tab / Shift+Tab | indent, or out again |
| Alt+Shift+↑/↓ | move a row among its siblings |
| ⌘+Z | take back your last edit here |
| ⌘+Enter | tick it off, or take that back |
| ⌘+⇧+Enter | walk the mark on: to do, then doing, then none |
| Shift+Enter | write the note under it |
| Esc | drop what you were typing |
| ⌘+⇧+Z | put that edit back |
Undo is an inverse rather than a restore: it replays the edit that reverses yours, judged against the outline as it is now — so it never takes back what an agent or another tab did in the meantime, and one that no longer fits says why.
Not everything has a key. Hover a row and the ••• in the
gutter carries the rest: the three marks and clearing one, setting,
changing or clearing a date — the same picker the row’s own pill opens —
retiring one placement of a mirrored node, archiving a subtree — it asks
first, and says how many rows go with it — and copying the subtree as
tab-indented plain text. Those go on the same undo stack, so ⌘Z takes a
menu edit back too. There is no delete: archiving moves the subtree to an
Archive.jsonl keeping its ids, so nothing that pointed at it
breaks. Every one of those is the same operation an agent would perform,
which is a rule olai holds itself to: both faces can do the same things to a
directory.
Preferences, and fifteen palettes
One door in the app header, and two rows behind it: the theme, and what a page does with finished work. Both are this browser’s, which is the panel’s own first claim about itself: two machines reading the same outlines are entitled to look different, and the served directory neither knows nor cares.
chalk is in force. Every colour on the page is this one table, so a pick repaints all of them at once.
Pages open with finished work shown. Any page’s own Done switch still overrides it.
These are this browser’s. They are stored here, reach every tab you have olai open in, and are never sent to the server.
The same rows, in two of the fifteen. A palette is
eight colours with a name; picking one writes data-theme on
<html>, and the stored pick lands there before the
first paint, so a reload never flashes the wrong one.
Seven of the fifteen are dark grounds. There is no
system chip and no auto: the operating system’s preference is
not consulted at all, because two ways to be dark are two answers that
can disagree — with each other, and with the reader who already said
what they wanted. A page nobody has picked on reads in
chalk, which is the default and the one palette that
promises WCAG AA on every pair it paints; a unit test holds it to that,
pair by pair.
Done: Visible or Hidden is a default, not a second switch. A reading belongs to a page — folds start fresh when you zoom — but “I do not want to look at finished work” is a claim about the reader, and pressing it on every page opened is what a preference exists to stop. So it moves the page you are on and leaves alone any page whose own Done switch you have already pressed. Hiding finished work is a row not drawn: never a node marked, never a file written.
Agents, over ACP
The chat panel connects to any agent that speaks
ACP. Claude Code is bundled as
the default; set OLAI_ACP_AGENT to use a different agent CLI.
Olai hands an agent no filesystem — a fixed set of tools that can only name
nodes — so every write it asks olai for is validated before it lands. What a
coding assistant brings with it is its own: it edits files on the authority
it already has in a terminal, and olai neither grants that nor pretends it
away. It shows it.
Files go in by paste, by drag, or by the picker: drag a screenshot, a PDF or a text file onto the conversation — several at once if you like — and the panel lights up the area that will take it while you are still holding it. The agent is handed the path to read, not the bytes. They live in a temporary directory of that conversation's own, never under the directory being served, and go away with it. A picture shows itself; a document shows its name and size. Anything olai will not take is refused where you dropped it, by name.
A tool call is one folded line, and what the call changed is not folded away. There are two kinds of change and the panel draws them differently, because they are different things. A write through olai’s node tools shows what changed about the node, in the same words the commit panel uses for the same edit — marked done, note rewritten, moved — and the outline it landed in. A file the agent rewrote with its own tools shows a diff: the path, how many lines came and went, and the change itself, with the unchanged stretches collapsed so the first thing you read is what moved. It is trimmed to a few rows and expands where it stands. An outline never gets a text diff — one line per node means a text diff of one is a single enormous line with everything on it changing at once.
cabinets.md+3−2
+4 more lines
The open page updates because the file on disk changed. The keyboard and the agent write through the same operations layer.
Git integration
Writes go to disk immediately and are committed when you
choose — everything the repository has waiting, outlines and the files
beside them, ticked one by one if you want a smaller commit, and pushed
from the same panel. Commit messages are prefixed with olai:,
so git log --grep '^olai' shows the audit trail.
┌─ Changes ─────────────────────────────────┐
│ olai: Outlines as a collection done │
│ · chat agent · 12m ago · 1a2b3c4 │
│ │
│ OUTLINES ───────────────────────────── │
│ ☑ roadmap.jsonl │
│ ✓ Outlines as a collection done │
│ ✎ Notes: one state, same line note │
│ + Kolu integration: auto-… created │
│ │
│ OTHER FILES ────────────────────────── │
│ ☑ README.md modified │
│ ☐ notes/scratch.md untracked │
│ │
│ 2 commits not on origin/master [ Push ] │
│ [ Commit 3 changes · 1 file ] │
└───────────────────────────────────────────┘
$ claude mcp add olai -- olai mcp ~/outlines
The same tools are available over MCP for agents running in a terminal. There is no write CLI.