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 line

Execution 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; silent runs without storing; none / discard throw 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-run

Preview 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=5

A 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=42 gives $n = 42).
  • Keys must be valid environment names ([A-Za-z_][A-Za-z0-9_]*).
  • A :var value 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.
  • --var overrides a :var of the same name; a dependency with no stored result is an error telling you which block to exec first. 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 ./output

With --out-dir ./output, the resulting output/scripts/build.sh contains:

set -eu
echo compiling

Choose 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.