fan-out & merge
review every changed file in parallel with one cheap agent each, then a synthesizer merges the per-file findings into one report.
run one agent per item across a computed list, all at once, then hand every result to a single synthesizer. this is the parallel shape, the most common orchestration workflow.
the scenario
a pull request touches several files. you want each file reviewed independently, in parallel, then the findings merged into one prioritized report with overlaps removed. each reviewer is a cheap one-shot agent that writes its findings to a file and exits; the synthesizer reads them all and produces the summary.
the annotated recipe
import { orcr } from "@orchestratr/sdk";
import { execSync } from "node:child_process";
import { readFile } from "node:fs/promises";
const files = execSync("git diff --name-only main", { encoding: "utf8" }).trim()
.split("\n").filter(Boolean);
if (files.length === 0) { console.log("No changed files."); process.exit(0); }
await orcr.scope("review", async () => {
const reviewers = await Promise.all(files.map((f, i) =>
orcr.agent.run({
agent: "claude", path: `fanout/file_${i}`, gc: "immediate",
prompt: `Review the diff of ${f} against main for bugs and risky changes.
Expand the environment variable ORCR_AGENT_DATA_DIR and write your
findings to $ORCR_AGENT_DATA_DIR/response.md, then say DONE.`,
})));
// settles when every reviewer finishes: gc:immediate → ended (completed)
await orcr.agent.wait("fanout/*"); // relative to the review scope
const findings = await Promise.all(reviewers.map(async r =>
`## ${r.path}\n` + await readFile(`${r.dataDir}/response.md`, "utf8")));
const summary = await orcr.ask({
agent: "codex", path: "merge/synthesizer",
prompt: `Merge these per-file review findings into one prioritized report,
deduplicating overlaps:\n\n${findings.join("\n\n")}`,
});
console.log(summary);
});what each piece is doing:
- the fan-out is
Promise.alloverfiles.map(...). eachorcr.agent.runreturns a handle immediately, so all reviewers are spawned before any awaits complete, and they run concurrently up to the concurrency caps. - the
pathon each run interpolates the index, so reviewerilands atfanout/file_0,fanout/file_1, and so on under thereviewscope:review/fanout/file_0,review/fanout/file_1. distinct paths are mandatory; two agents cannot share one live path. gc: "immediate"closes each reviewer's pane the moment its final response is captured. no parked panes to clean up.- the file convention. each reviewer writes structured output to
$ORCR_AGENT_DATA_DIR/response.md. the prompt tells the agent to expand the environment variable; the caller reads the same location from the handle'sdataDir. the completion sentinel (then say DONE) tells the agent when it is finished. see data & file conventions. orcr.agent.wait("fanout/*")blocks until every direct child ofreview/fanoutsettles. forgc: "immediate"agents that meansended (completed). the pattern is relative to the scope;*matches one whole segment, so it catchesfile_0throughfile_Nbut nothing nested deeper.- the synthesizer reads each
response.mdoff disk (not from a transcript) and merges them with a singleorcr.askon a different provider.
the glob is whole-segment
fanout/* matches direct children only. to wait on everything under the scope at any depth you would use fanout/**. quote patterns when you type them in a shell so it does not expand them against real files. see path grammar & glob patterns.
primitives it exercises
orcr.scopeto root the fan-out and namespace every reviewer.- parallel
orcr.agent.runviaPromise.all, each withgc: "immediate". orcr.agent.waitwith a*pattern to join on the whole batch.- the data-directory reads via each handle's
dataDir. orcr.askfor the single synthesizer.
failure and cleanup
gc: "immediate" is the whole cleanup story here: each reviewer self-cleans as soon as it finishes, so there is nothing to reap even if the batch is large. the synthesizer is an ask, which is also gc: "immediate" under the hood.
two things to know:
- if a reviewer's
response.mdis missing when you read it,readFilethrows. because the flow is not wrapped inkillOnThrowhere, add it if you spawn a large batch and want a thrown read to tear the batch down:orcr.scope("review", async () => { ... }, { killOnThrow: true }). wait("fanout/*")snapshots its targets at invocation. reviewers spawned after the wait starts are not included, which is why the fan-out completes before the wait is called.
run it
orcr scaffold review-workflow && cd review-workflow
# paste the recipe into workflow.ts, replacing the generated example
npx tsx workflow.tswatch it live by splitting a pane running orcr top "review/**" beside yourself; the tree lights up as each reviewer starts and settles. see monitor with orcr top.
this is also the first-workflow tutorial, built step by step. next: generate-and-filter fans one prompt across providers instead of many files across one.
fix-until-green
a worker agent fixes build errors while a verifier on a different provider judges the result, looping until the verifier says PASS.
classify-and-act
one cheap classification routes each item to a per-class handler, normalizing untrusted model output through an enum before it touches a path.