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 format — json 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.
@table |rows @downloads|
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 --fragmentResult. 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-inenvsilently 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
licurrently 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 tableis a focused excerpt of this recipe; Embed the Wit parser crosses the same data seam in-process.