ஓலை — the palm leaf Tamil was written on for two millennia. Nodes are leaves; your files are the manuscript.
An outliner for people who think the filesystem was right all along.
Self-hosted, AI-native alternative to
Workflowy. Your outline is a bunch
of #lang olai files in a git repo. You edit them with
$EDITOR or point a coding agent at them. A small
Racket server renders the tree to your
phone. That's it. No cloud, no accounts, no Electron.
Workflowy got the data model right (one big tree, mirrors, dates) and the ownership model wrong (their server, their format, their AI). olai inverts it:
#lang;
the expander is the validator.Mirrors fall out of the language for free: a node is a binding, referencing it twice is a mirror. Define-before-use kills most cycles before they exist.
Quoteless outline, the flagship surface. Nest with 2 spaces; attach notes and dates under a title.
#lang olai
Inbox #capture
: Quick capture landing zone
Buy milk — don't quote me
@date 2026-08-04T18:00
[x] Already shipped the pitch
Agent work ^agent
Wired the CLI
@done 2026-08-03
This week
*agent ; a mirror, not a copy
Titles and notes are Markdown at render time (web view only);
stored strings stay raw. Check off with [x] or
@done — one node, one of them. A title-trailing
^anchor declares a node; a line that is only *anchor
is a mirror of it, reaching anywhere in the loaded tree,
@included fragments too.
Under the hood every outline becomes s-expressions. Same expander, same error messages:
#lang olai/sexp
(t "Inbox #capture"
#:description "Quick capture landing zone"
(t "Buy milk" #:date "2026-08-04T18:00")
(t "Already shipped the pitch" #:done)
(t "Wired the CLI" #:done "2026-08-03"))
They are the only ones. Every command replies in JSON — always, with or
without --json, which is still accepted and does nothing — and
errors are the error object on stderr. Exit codes are contract: 0 ok,
1 usage, 2 the outline said no, 3 file not found. JSON
fields are append-only within a version. No ANSI, no terminal
tree, no plain mode — the human view is the web app.
$ olai tree examples/Example.rkt
{
"version": 1,
"file": ".../Example.rkt",
"tasks": [
{
"title": "Inbox #capture",
"date": null,
"description": "Quick capture landing zone",
"done": null,
"status": "open",
"id": null,
"key": "pd076e677",
"tags": ["capture"],
"children": [ ... ]
}
],
"anchors": { "agent": { "title": "Agent work", ... } },
"task_count": 12, "mirror_count": 1, "anchor_count": 1
}
key is a node's stable identity — its ^anchor,
else a hash of its defining file plus the ordinals that reach it. It survives
renaming the node or any ancestor. The web view addresses nodes by it.
| command | what it does |
|---|---|
check | validate the module(s); the language is the only validator |
tree | the task forest |
agenda | overdue / today / upcoming, merged across files |
calendar, ics | dated tasks by day; feed for your calendar app |
add, done, move, daily | the write path — auto-commits when the file's dir is a git work tree |
serve | the web view |
olai serve renders your outline over htmx and pushes changes
over SSE: a file moves on disk, every open tab redraws with no refresh. It is
installable as a
PWA
(manifest, icons, theme-color) — live view only, no offline shell, so it
still wants the network. No auth; the network is the auth, so bind it to
localhost or put it behind Tailscale or
Caddy.
The page itself writes nothing yet. Capture and check-off in the browser are next; until then the chat panel — Claude Code driven over the Agent Client Protocol, one turn at a time — is how you change an outline without an editor.
$OLAI_HOME/*.rkt <- personal data (#lang olai)
| ^
v | edits your files
olai CLI (Racket) | <- validate / query / capture
| |
v |
racket web-server --- spawns ---> ACP agent (JSON-RPC on stdio)
|
+-- SSE ---> browser (htmx): a file moved, or the agent said something
With Nix, nothing to install:
$ nix run github:juspay/olai # the web view, on :8080
$ nix run github:juspay/olai#cli -- check Tasks.rkt # the CLI
Or from a clone:
$ nix develop # racket 9.2 + just; or install them yourself
$ just install # gregor + markdown, then --link olai/
$ just serve # $OLAI_HOME on http://127.0.0.1:8080
$ just check # validates $OLAI_HOME/*.rkt
$ just tree examples/Example.rkt
To leave it running, the flake ships a
home-manager
module — a systemd user unit on Linux, a launchd agent on macOS. It puts the
CLI on your PATH too, so the binary you query with cannot skew
from the service you read:
# inputs.olai.url = "github:juspay/olai";
{
imports = [ olai.homeManagerModules.default ];
services.olai = {
enable = true;
dataDir = "${config.home.homeDirectory}/outlines"; # required
# host = "127.0.0.1"; port = 8080; the defaults
};
}
Your data lives outside the repo: point OLAI_HOME at the
directory holding your *.rkt outlines. There is no default —
unset, the write commands say so, and the repo serves its own
examples/ plus Roadmap.rkt instead. Sync that
directory however you like: git, Dropbox, rsync. Single user, many devices.
Alpha — the author's daily driver, and nobody else's yet. No release, no version number, no upgrade path promised: this is a repo you clone at a commit. Nothing here is a product.
What works today: outline #lang olai + sexp core + agent CLI.
Done status, mirrors and @include composition are first class.
The web view reloads on file change, pushes over SSE, installs as a PWA, and
carries the chat panel. What does not: the page writes nothing — capture and
check-off in the browser are still to come — there is no static HTML export,
no auth, and no multi-user story (there will not be one; it is single user by
design). Ancestor: srid/Tend.
The project tracks its own plan the way it wants you to track yours:
Roadmap.rkt
at the repo root is a #lang olai outline, edited and committed
like any other file.