- section
- authoring
- created
- 2026-07-26
- updated
- 2026-10-30
Babel
Babel
Babel runs the fenced code blocks in a note and keeps their results, so a note can be executed, not just read — track's take on Org Babel, in plain Markdown. Results are stored in the note's sidecar metadata, never written back into the Markdown body, so the source stays exactly as you typed it.
A runnable block
Any fenced code block whose info string starts with a language is a Babel block. Options follow the
language as Org-style :key value header arguments, keeping the fence a valid Markdown code block:
```lua :name hello :results output
print("hi from a note")
```Running it
Execute a block with the CLI, choosing one by :name, position, or cursor line:
track babel exec --id <note-id> --name hello
track babel exec --path note.md --ordinal 0 # 0-based block index
track babel exec --path note.md --line 12 # block containing this 0-based lineExecution is opt-in and never happens on its own. A block is subject to its :eval policy:
:eval yes(default) — runs when you invoke it.:eval query— refuses unless you pass--yes.:eval no— never runs.
--timeout caps each run (30s by default), and :dir runs the block relative to another directory
(constrained to the vault).
Results
:results decides what is captured and kept, as a set of tokens (default output replace):
output— captured stdout/stderr and exit status.verbatim— the raw value, stored as text.replace— keep only the latest result;silentruns without storing;none/discardthrow the result away.
Stored results live in the sidecar keyed by the block. List what a note has kept with:
track babel restore --id <note-id>Markdown export (track export) selects source and matching stored results with
:exports code|results|both|none. The Web workspace and static export-site currently preserve
source blocks and do not apply these headers or render stored results. In Neovim results appear as
virtual lines below the block; none and discard suppress them.
:cache yes reuses successful results when source, headers, noweb expansion, variables, executor
configuration, and working directory match. The default is no: external files and ambient environment
are not tracked. Old results without input hashes need one rerun. Header/dependency changes and runs
with different CLI inputs are not restored as if they matched the written block.
Preview source and inputs without running or saving anything:
track babel run --id <note-id> --name hello --dry-runPreview also works with :eval no and without an executor. Neovim restores against the current buffer;
CLI integrations can use babel restore --body-stdin for unsaved content.
Composing blocks: noweb references
A block can pull in other named blocks with Org's noweb syntax: <<name>> is replaced by the body of
the block named name, recursively, before the code runs. Expansion is opt-in per block through
:noweb:
:noweb no(default) —<<...>>is left as literal text.:noweb yes— expand before executing and before tangling.:noweb eval/:noweb tangle— expand only in that phase.
```sh :name greeting
echo track
```
```sh :name main :noweb yes
<<greeting>>
echo compiling
```Running main executes both lines. A reference standing alone on a line keeps its indentation (every
expanded line is re-indented to match), an unknown name is an error, and a reference cycle is refused
with the chain spelled out, e.g. noweb cycle: a -> b -> a.
Calling blocks with variables
track babel run calls a named block with parameters:
track babel run --id <note-id> --name shout --var excitement=5A block can also declare its own inputs with :var header arguments. A value that names another
block in the note feeds in that block's stored result — run the dependency once with exec, and
its output becomes the variable:
```sh :name shout :var name=greeting :var excitement=3
printf '%s%s\n' "$name" "$(printf '!%.0s' $(seq "$excitement"))"
```With greeting's stored result being track, calling shout prints (real output):
{"id":"shout","status":"success","stdout":"track!!!\n","exit_code":0,"stored":true}Variables reach the block as environment variables, the one mechanism every configured language shares. That sets the limits:
- Values are always strings; numbers arrive as their decimal text (
--var n=42gives$n=42). - Keys must be valid environment names (
[A-Za-z_][A-Za-z0-9_]*). - A
:varvalue in the fence info string cannot contain spaces (the info string splits on whitespace); pass spacey values from the CLI, where--var msg="hello world"works. --varoverrides a:varof the same name; a dependency with no stored result is an error telling you which block toexecfirst. Dependencies are never executed automatically.
track babel run and track babel exec are the same command under two names, so :var and --var
work with either.
Tangling source to files
A block carrying :tangle <path> supplies the complete file content. If multiple blocks in one
note resolve to the same file, the last block wins, replacing the entire content. The JSON plan
reports the winning source and earlier overridden locations; blocks includes both. Locations
contain a source path, 1-based fence line, 0-based block ordinal, and optional name.
Compose fragments explicitly with textual noweb references:
```sh :name prelude :tangle no
set -eu
```
```sh :name compile :tangle scripts/build.sh :noweb tangle
<<prelude>>
echo compiling
```track babel tangle --file setup.md --out-dir ./output --dry-run
track babel tangle --file setup.md --out-dir ./output
# Plan several source files together; shared output paths across files are errors.
track babel tangle --file shell.md --file git.md --out-dir ./output
# Without --out-dir, return a new temporary output directory.
track babel tangle --path setup.md
# A configured vault note can use the same output-root options.
track babel tangle --id <note-id> --out-dir ./outputWith --out-dir ./output, the resulting output/scripts/build.sh contains:
set -eu
echo compilingChoose one selector: repeatable --file, single-file --path, or vault-note --id. They cannot be
mixed. --file and --path read Markdown directly without configuration, a vault, a database, or
HOME. Input symlink aliases are canonicalized; the same input file is processed only once.
--out-dir selects the output root; a relative directory is resolved against the current working
directory. :tangle targets are relative to that root, and missing parent directories are created.
Absolute targets must also stay inside the output root. Without --out-dir, track creates a unique
track-tangle-* directory under the system temporary directory. The JSON response contains its
absolute output_dir, temporary: true, dry_run, and targets. After a successful real run, the
caller owns cleanup. A failure or dry-run removes a temporary root; the preview path returned by
dry-run therefore no longer exists. An explicit root has temporary: false and is never removed
by this cleanup.
All destinations are validated before output files are written, including symlink resolution. Targets outside the output root are refused. When writing inside an existing vault, track metadata and managed note files remain protected. Aliases to one output count as the same target. Within one source file the last block wins, including during a multi-file invocation; different source files sharing a target cause the entire plan to fail, regardless of input order. A planned file cannot also be a directory for another output. An I/O failure during writing can leave earlier files updated.
:tangle yes is not supported; name the file explicitly. Omitted :tangle and :tangle no produce
no file. Tangling never runs blocks or reads their stored results; noweb expansion composes source
text within each input file, with no cross-file lookup or automatic dependency execution.
For a complete shell-and-Git configuration note with tangle and static-site instructions, see the literate dotfiles example. It generates files in a caller-selected directory; home-directory installation and Nix integration remain separate.
Languages
lua and viml ship as ready-to-run sample executors. Add any other language by pointing an
environment variable at its interpreter — TRACK_BABEL_<LANG> — for example:
export TRACK_BABEL_PYTHON="python3"
export TRACK_BABEL_SH="bash"A block whose language has no executor configured is left unrun with a clear error, so nothing executes by surprise.