nums = [1, 1, 2, 3, 5, 8, 13] print(sum(nums) / len(nums))
the idea is
embarrassingly small.
You're outlining a problem. You want to verify a calculation. Today: tab to a REPL, paste, run, tab back, paste the result. Maybe annotate. Two days later you can't tell which number was hand-typed and which one was computed.
outl removes the tabbing. The code lives in your note. The result lives in your note. The note is the notebook.
A fenced code block isn't syntax highlighting. It's an invitation.
press ▷ on this.
```python
from datetime import date
commits = [42, 17, 31, 9, 28, 14, 22]
total = sum(commits)
avg = total / len(commits)
print(f"{total} commits · avg {avg:.1f}/day")
``` a91f2c3 auto-run:: on
The button on the right is the same key as gx in the TUI. Or set auto-run:: on on the block and it runs every time the page opens — cache-aware, so unchanged source returns the cached result in <1ms.
four languages ship.
the fifth is a build flag.
Each language is an outl-exec adapter — about 80 lines of Rust. The runtime registry is plugin-shaped: a new language ships as a new crate, and each one is its own cargo feature, so a build can drop what it doesn't want.
lang-lisp is the one that is off by default, and the reason is written into the crate rather than into a changelog nobody reads. Steel gives an embedder nothing to build a host boundary out of: Engine::new_sandboxed() still registers the whole steel/meta module, so a fence can construct an unsandboxed engine inside the sandboxed one and run a shell through it. What outl ships instead is a denylist held shut by a test — good enough for fences you wrote, not good enough to be on for everybody.
const days = (a, b) =>
(b - a) / 864e5;
days(new Date('2026-01-01'), new Date()) local t = {1, 2, 3, 4, 5}
local sum = 0
for _, v in ipairs(t) do sum = sum + v end
return sum fn main() {
let v: Vec<i32> = (1..=10).collect();
println!("{}", v.iter().sum::<i32>());
} (define (sq x) (* x x)) (map sq '(1 2 3 4 5))
auto-run
and the hash cache.
Set auto-run:: on on a block. Every time the page opens, the block evaluates — but only if its source changed.
a91f2c3a91f2c3 → "194 commits · avg 27.7/day"a91f2c3
The hash is stamped into the result subblock as source-hash::. If you edit the source, the hash changes, the cache misses, the block re-runs. Deterministic and observable.
a fence is not
always yours.
It is easy to read "code in your notes" as code you typed. Most of it is. But a fence body also arrives over iroh from a device you paired, through outl import from somebody else's graph, and from an agent driving outl over MCP. Pairing a phone is a decision about sync. For a while it was silently also a decision about what could run on your laptop.
So the boundary is per language, and it is worth saying plainly which is which rather than calling the whole thing sandboxed:
Compiled to wasm32-wasip1 and run in wasmtime with WASI cut to the bone: no preopens, so your filesystem does not exist from inside; no env, so no $HOME; no sockets; stdio captured in memory. Whatever it prints is the result block.
No wasm layer — these are interpreters embedded in the binary. Python is built without the stdlib and JavaScript with no host bindings, so neither has a file or a socket to reach for in the first place. Lua does, and that one had to be taken away.
the one that was open
Lua's interpreter was built with mlua's default set, StdLib::ALL_SAFE. "Safe" there means memory-safe, not sandboxed — it excludes debug and ffi and includes os, which carries execute, plus io and package. A ```lua fence had a shell, arbitrary file read and write, and getenv. It took 7ms to prove.
The fix is an allowlist — string, table, math, utf8, coroutine — because a denylist fails open: the next release of the binding that adds a library would grant it without anyone deciding to. The loaders (load, loadfile, dofile, require) come off separately, since Lua's base library is not covered by any flag and a loader re-opens what the allowlist closed. Reviewing that fix found the same hole in Lisp, which nobody had looked at. That is why Lisp is now a build flag.
os.date, os.time and os.clock went out with os.execute — they ship in the same flag. A Lua block that formatted a date now traps on a nil os. Handing back the clock half as a built table is worth doing, and it is a feature rather than part of closing a hole, so it stayed out.
and the one that never stopped
The runtime contract said implementations must honour a deadline. The helper that enforced it had zero callers, and three of four runtimes took the argument and dropped it. while true do end was still going after twenty seconds — and because execution is in-process and synchronous, that is not a stuck block. It is the TUI event loop frozen, the desktop wedged holding the workspace mutex, and outl mcp serve hung with its agent waiting on it.
Lua runs under a global instruction hook — global, because a hook installed per-thread is silently dropped for any coroutine the script creates. Rust stops on wasmtime epoch interruption. In both, the block unwinds and nothing survives it.
None of the three embeddings offers a cancellation point that works: boa's limits are per-loop, not wall-clock; rustpython's eval breaker is crate-private; steel takes an interrupt flag its VM never reads. So the deadline frees your client and the worker runs until the process exits. That is a trade, and the alternative is not "no leak" — it is a frozen editor.
The whole story — including the cancellation API that is written like a cancellation point and never read by the VM behind it — is in a fence is not always yours.
vs the usual
suspects.
| feature | outl | Jupyter | Notion | Dataview | Logseq | Observable |
|---|---|---|---|---|---|---|
| markdown is the source of truth | ✓ | .ipynb (JSON) | ✕ cloud | via .md | via .md (with id::) | .ocnb (JSON) |
| multi-language native | 4 + 1 flag | via kernels | ✕ | JS only | limited | JS only |
| result lands as markdown | ✓ > result: | output cell | cell | rendered | limited | cell |
| source-hash cache | ✓ SHA-256 | manual | ✕ | memoized | limited | live |
| sandboxed by default | wasm + allowlist | ✕ host | ✕ cloud | host JS | host | ✕ host |
| a fence gets a deadline | ✓ per language | ✕ | ✕ | ✕ | ✕ | ✕ |
| works offline, no account | ✓ | ✓ | ✕ | ✓ | ✓ | limited |
| open source | MIT | BSD | ✕ | MIT plugin | AGPL | ✕ partial |
- +markdown is yours forever — the source format your editor already speaks
- +four languages in the binary, no kernel install
- +a boundary and a deadline on every fence, default on
- +native binary, no Electron tax
- −no rich output yet — matplotlib plots, dataframes, images are stdout-only
- −no shared kernel state across blocks — each block is its own ephemeral runtime
- −no third-party Python packages (RustPython subset; no numpy yet)
- −no interactive widgets — the result is text
run code,
keep markdown.
One binary. Four runtimes in it, a fifth behind a flag. Every fence bounded, every fence on a deadline.
Code blocks executed inside notes + an MCP server the LLM can call: your notes stop being a passive archive and start being a programmable second brain.