Wit

A report with live data

Build a release note or status report whose version number, contributor list, and metrics table all come from outside the document — programs and files resolved at build time instead of pasted in by hand. You write the prose once; the numbers refresh themselves every time you build.

This recipe is for writers, with a developer wiring the data sources once.

Step 1 — Name each input by alias

Inside the document you only ever name an alias. An alias can never name a command, which is exactly what keeps the document safe to share.

@load meta load@
@load team load@
@load downloads load@

Step 2 — Wire the aliases

A developer maps each alias to a program in wit.sources.json. Every entry has a run (the command as an argument array) and a formatjson or csv.

{
  "sources": {
    "meta":      { "run": ["node", "scripts/meta.js"], "format": "json" },
    "team":      { "run": ["node", "scripts/team.js"], "format": "json" },
    "downloads": { "run": ["cat", "data/downloads.csv"], "format": "csv" }
  }
}

A JSON source that prints an object becomes a record; one that prints an array becomes a collection; a CSV source becomes a collection of records keyed by the header row.

Step 3 — Use a loaded record in prose

Pull a field straight into a sentence with dotted access.

@h1 Release @meta.version h1@

Cut @meta.date on branch @env.WIT_BRANCH.

The built-in env source needs no entry in the config — it exposes environment variables (loaded from an .env file when you pass one).

Step 4 — Iterate a loaded collection

Loop the contributor list into li items.

@h2 Contributors h2@

@ul
(each @team as person)
@li @person.name — @person.commits commits li@
(end)
ul@

Step 5 — Drop a CSV straight into a table

A loaded CSV collection renders as a table with no hand-typed rows.

source
@table |rows @downloads|
result

Step 6 — Place an image beside prose

row

is an invisible flex band; each col with a fixed size sits beside flowing text — no float, no clear.

@row
  @col |size 260|
    @img |src assets/downloads.svg| |alt Downloads chart|
  col@
  @col
    macOS leads this cycle, with Windows a steady second and Linux climbing.
  col@
row@

Step 7 — Build with the sources config

Point the build at the config, hand it the environment file, and grant it permission to run the programs with --allow-exec.

wit build report.wit --sources wit.sources.json --env .env --allow-exec --fragment
Result.

A document that opens with <h1>Release 0.4.0</h1> and <p>Cut 2026-07-05 on branch main.</p>, a contributor list (TauraJ — 42 commits, Ada Lovelace — 17 commits), a downloads table with header-derived columns, and the image-beside-prose band — all verified from the worked example. Swap --fragment for -o report.pdf to get a styled PDF.

The trust boundary

The document names only an alias. Only the operator's wit.sources.jsonmaps that alias to a command, and --allow-exec must be passed for any program to run. Without it, only the built-in env source resolves. That indirection is the security model — a document you receive can never run a program you didn't wire yourself.

Pitfalls

  • Forgetting --allow-exec. Every alias other than the built-in env silently fails to load without it.
  • Noisy sources. A JSON source must print only JSON to standard output — a stray log line corrupts the parse.
  • Stray list wrapper. An iterated li currently renders inside an auto-inserted paragraph. It is cosmetic and harmless in a browser; note it rather than fight it.

Variations

Any program in any language that emits JSON, CSV, or lines works — a Python script, a SQL client, a compiled binary. Only the value model (records, collections, scalars) crosses the seam. To embed this same data flow in your own app without the CLI, see Embed the Wit parser.

See also

Load a CSV into a table

is a focused excerpt of this recipe; Embed the Wit parser crosses the same data seam in-process.