External data — the DataLoader seam
@load is the one seam between a Wit document and
the outside world. Whether the data comes from a CLI subprocess or an in-process
object, the rest of the pipeline never knows the difference — by the time resolve runs, loaded data is indistinguishable from a hand-written #name:.
Concept
@load <alias> load@ names data by alias only; a host supplies the
value. loadExternalData(doc, source) runs after parse,
before resolve and rewrites every @load into an ordinary DataDef.
Signatures
loadExternalData(doc: Document, source: DataSource) → Document.type DataSource = DataLoader | Record<string, unknown>— a loader function or a plain dict{ alias: value }(the simplest embedded case).type DataLoader = (req: DataLoadRequest) => unknown.interface DataLoadRequest = { alias: string; args: Record<string, string>; loc: Loc }.argsare the named captures —@load cite |key devlin2019| load@gives{ key: 'devlin2019' }.|as name|rebinds the resultingDataDefname;|from alias|supplies the alias when there is no body prose.toDataValue(value: unknown, loc: Loc) → DataValue— normalises plain JSON into the Wit value model. It is the only shape that crosses the seam.
Basic — the dict form
The simplest source is a plain object keyed by alias:
import { parse } from '@witlang/parser';
import { loadExternalData, resolve, expand } from '@witlang/runtime';
const doc = loadExternalData(
parse('@load meta load@\n\n@meta.version', 'report.wit'),
{ meta: { version: '0.4.0' } });
// the first child is now dataDef{ name: 'meta', value: record{ version: '0.4.0' } }
renderHtml(expand(resolve(doc))); // → <article class="wit-doc"><p>0.4.0</p></article>Realistic — the function form
For dynamic data, pass a loader. It is called once per @load,
lazily, per alias:
const loaded = loadExternalData(doc, (req) => {
// req = { alias, args: { …captures }, loc }
return fetchSync(req.alias, req.args); // your synchronous I/O
});Edge — toDataValue
toDataValue is how plain JSON becomes a DataValue: an
object becomes an ordered Record, an array a Collection,
primitives become scalars, and null or undefined become NullValue.
import { toDataValue } from '@witlang/runtime';
toDataValue({ a: 1, b: [2, 3] }, loc);
// → record{ fields: [ { a: numberValue 1 },
// { b: collection[ numberValue 2, numberValue 3 ] } ] }Every option / edge
- Dict source, missing alias. A dict with no entry for an alias
throws
no data provided for alias "x", which surfaces asE_LOAD_FAILEDat the load site. - Throwing loader. Any error your loader throws becomes
E_LOAD_FAILED, carrying the message and the loadloc. - Downstream, loaded data is ordinary data. It flows through
@meta.version,(each @team as p),@table |rows @downloads|, and interpolation with no special casing. - Raw load is skipped. A
@@loadraw node is not treated as a load (the guard checksraw !== true). - The CLI is one implementation of this seam. Its
makeDataLoaderreadswit.sources.json, runs the aliased program (gated by--allow-exec), and parses stdout by declared format;envis a built-in source returning the process environment. See CLI reference.
Common mistakes
- Expecting
loadExternalDatato fetch. It only calls your loader or reads your dict — you own the I/O. - Returning a class instance instead of plain JSON. Only object, array, and
scalar values cross via
toDataValue. - Forgetting it must run before
resolve. Order is parse → load → resolve → expand → render.
See also
Resolve & expand · Embed Wit in an app · CLI reference (the wit.sources.json schema) · AST reference (DataValue).