code execution

your notes,
they compute.

No Jupyter. No Electron. No Notion blocks. A fenced code block inside a markdown bullet runs in four languages out of the box, drops the result back into the file, and stays diff-able forever.

I
act one

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.
II
act two

press ▷ on this.

~/notes/launch-day.md ▷ run
title:: launch day math
• how many commits this week?
```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")
```
idle
› result:
● cached · hash: a91f2c3 auto-run:: on
• the result is a markdown blockquote. open the file in vim — it's still markdown.

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.

III
act three

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.

Py Python RustPython
```python
nums = [1, 1, 2, 3, 5, 8, 13]
print(sum(nums) / len(nums))
```
› result: 4.714285714285714
Scientific snippets, quick stats, list comprehensions — without spinning up Jupyter.
in-process · built without the stdlib, so there is no os, no open, no socket to reach for
JS JavaScript Boa · ES2015+
```js
const days = (a, b) =>
  (b - a) / 864e5;
days(new Date('2026-01-01'), new Date())
```
› result: 146
Web math, date arithmetic, regex play — exactly what you'd open the browser console for.
in-process · a plain engine with no host bindings — ECMAScript and nothing else
Lua Lua mlua 5.4 · vendored
```lua
local t = {1, 2, 3, 4, 5}
local sum = 0
for _, v in ipairs(t) do sum = sum + v end
return sum
```
› result: 15
Embedded scripting feel. Fast. Familiar if you've ever touched Neovim or Redis.
in-process · stdlib allowlist (string, table, math, utf8, coroutine) and the loaders stripped
Rs Rust rustc → wasm32-wasip1 → wasmtime
```rust
fn main() {
    let v: Vec<i32> = (1..=10).collect();
    println!("{}", v.iter().sum::<i32>());
}
```
› result: 55
Compiled and cached as wasm in ~/.cache/outl/runtimes/. ~20× faster re-runs.
wasm · wasmtime with WASI cut to the bone — no preopens, no env, no sockets
() Lisp lang-lisp Steel · Scheme R5RS-ish
```lisp
(define (sq x) (* x x))
(map sq '(1 2 3 4 5))
```
› result: '(1 4 9 16 25)
Tiny DSLs, symbolic computation, transformation pipelines — homoiconic notes. Build it in when the only fences you run are ones you wrote.
in-process · sandboxed engine plus a denylist, which is why it is not in the default build
IV
act four

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.

first open
• page loaded
▷ exec python · sha:a91f2c3
› result rendered · 18ms
● cached a91f2c3 → "194 commits · avg 27.7/day"
second open · source unchanged
• page loaded
● cache hit · sha:a91f2c3
› result rendered · 0ms
· no runtime spinup, no work

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.

V
act five

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:

rust
wasm, in wasmtime

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.

python, javascript, lua
in-process, behind an allowlist

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.

breaking, and worth knowing

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.

real abort lua, rust

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.

caller released, worker leaks python, javascript, lisp

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.
VI
act six

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
where outl wins
  • +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
where outl loses (today)
  • −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.