The build starts with one root action:
CommandLine.invoke({ argv: ["build"] })
Everything else is sync composition. This post follows the full execution from CLI input to terminal output.
cli.sync.ts inspects argv in a where clause, parses the subcommand, and issues a Commanding.issue action:
argv = ["build", "--source", "example/pages", "--output", "example/dist"]
→ Commanding.issue({ name: "build", args: { source, output, layouts, public } })
The invocation is linked to the command via CommandLine.waitFor. When the command later succeeds or fails, that outcome propagates back to the invoking process.
Invalid subcommands or missing required arguments trigger CommandLine.fail immediately.
build.sync.ts matches Commanding.issue with name: "build". It extracts source, output, layouts, and public from the command args, then fires a sequence of setup actions:
Building.start
Filing.clear
Collecting.clear
Frontmattering.clear
Routing.configure
Each clear action resets a concept's state to prepare for a fresh build. Routing.configure sets the source prefix and index filename for later route derivation.
After initialization, the build sync fires three scans:
Filing.scan({ root: source, glob: "**/*.md" })
Filing.scan({ root: source, glob: "**/*.html" })
Filing.scan({ root: layouts, glob: "**/*.html" })
Filing.scan({ root: public, glob: "**/*" })
Each scan returns an array of entry IDs. The Filing concept stores each discovered file as an entry record with path, source tag, and modification time.
For each scan result, discovery.sync.ts fans out the entry array into one frame per entry, then fires Filing.read:
where: (frames) =>
frames.flatMap((frame) => {
const entryIds = frame[entries] as string[];
return entryIds.map((id) => ({ ...frame, [entry]: id }));
}),
then: actions([Filing.read, { entry }]),
From there, content.sync.ts takes over. For each content entry that was read:
Filing.read
→ Frontmattering.parse // split YAML frontmatter from body
→ Formatting.render // markdown → HTML
→ Routing.derive // file path → clean URL
→ Collecting.collect // store collection membership
Each of these happens independently. Formatting.render does not wait for Routing.derive. They both fire as soon as parsing completes.
Layout files follow a shorter path through templates.sync.ts:
Filing.read (layout file)
→ Layouting.define // register the HTML template by name
templates.sync.ts contains a join sync: RenderAndRouteTriggersApply. It fires when both Formatting.render and Routing.derive have completed for the same entry:
when: actions(
[Formatting.render, {}, { entry }],
[Routing.derive, { entry }, {}],
)
then: actions([Layouting.apply, { entry }])
Layouting.apply resolves the entry's layout, queries the rendered HTML and route, substitutes template variables, and produces final HTML as a new action output.
publishing.sync.ts matches Layouting.apply and fires Filing.write:
then: actions([Filing.write, {
entry,
path: `${route}/index.html`,
content: composedHtml,
}])
A page with route /blog/post writes to example/dist/blog/post/index.html.
Collection index pages (like this blog listing) need data from every entry, so they must run after all entries are collected. The barrier is Building.complete.
templates.sync.ts matches Building.complete, queries all entries grouped by collection, and re-applies the layout for each index page:
when: actions([Building.complete, {}, {}]),
where: async (frames) => {
// query Collecting for all entries per collection
// find pages with type: index
// inject collection entry data for the template loop
return frames;
},
then: actions([Layouting.apply, { entry, collectionData }]),
After index regeneration, the build sync fires:
Filing.cleanOutput // remove files not produced by this build
Commanding.succeed // mark the command successful
cli.sync.ts catches Commanding.succeed and propagates it to the waiting CommandLine invocation, which sets the process exit code to 0.
The current BuildCommand sync fires everything in one then list: scans, complete, cleanup, and succeed. If any scan returns an error, the success tail still runs. A build can fail to read files, render content, or derive routes, but still report success.
Fixing this requires splitting each stage into success-only syncs that gate on the previous stage's success. This is tracked in the Sync Layer issues.
Read From Markdown to Published Page to follow a single file through every concept in the pipeline.