your first workflow
build a parallel code-review workflow end to end, fanning out one reviewer per changed file and merging the findings.
this tutorial builds a real workflow: review every changed file in parallel, one agent per file, then merge the findings into one report. along the way you will use scopes, the file convention for structured output, and automatic cleanup.
you need a working setup (installation) with both claude and codex supported, and a git repository with some uncommitted changes against main.
1. scaffold the project
orcr scaffold review-workflow && cd review-workflowthis gives you a plain npm project with @orchestratr/sdk installed. you will replace the generated workflow.ts with the code below.
2. write the workflow
open workflow.ts and replace its contents:
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);
});3. what each part does
the scope. orcr.scope("review", ...) roots the whole workflow under the review path. every relative path created inside the callback lands under it: fanout/file_0 becomes review/fanout/file_0, and the wait target fanout/* resolves to the direct children of review/fanout. rooting under one descriptive path is how the parallel agents stay organized and avoid colliding with each other.
the fan-out. Promise.all over the changed files starts one reviewer each, at fanout/file_${i}. each carries gc: "immediate", so its pane closes as soon as its first turn completes and its final response has been captured. there is nothing to reap by hand.
the file convention. a prompt cannot rely on the agent's chat output for a guaranteed format, so it tells the agent where to write. the rule has two parts: the prompt tells the agent to expand $ORCR_AGENT_DATA_DIR and write to a named file (response.md) and to say DONE when it finishes; the caller reads the same path from the handle's dataDir and validates it. orcr guarantees the directory exists and is unique per agent (it mirrors the agent's path and ends in the uuid); the contents are entirely yours.
the wait. orcr.agent.wait("fanout/*") blocks until every reviewer settles. for gc: "immediate" agents that means ended with exit_reason: completed, which is exactly the done signal here. because completion is verified (working seen after the prompt, then stable idle, then the transcript settled), a reviewer is only reported done once its response.md is actually written.
the merge. orcr.ask spawns one synthesizer at merge/synthesizer, blocks for its answer, prints it, and closes its pane. ask is the request-and-response one-liner, so the synthesizer cleans itself up like the reviewers do.
4. watch it live
before you run it, split a pane in your herdr session and point orcr top at the workflow's subtree:
orcr top "review/**"orcr top draws the live tree: review at the top, fanout/file_0, fanout/file_1, and so on underneath, each row showing its status glyph, provider and model, and age. statuses update in real time as reviewers move from working to idle to ended, then the synthesizer appears under merge. it is view-only; you act on agents with the CLI verbs.
5. run it
npx tsx workflow.tsthe reviewers run in parallel (bounded by the concurrency caps), each writes its response.md, the wait returns when they have all finished, the synthesizer merges the files, and the report prints to stdout.
cleanup and failures
every agent in this workflow tidies itself: the reviewers use gc: "immediate" and the synthesizer runs through ask, so all panes close after capturing their final response. no explicit kill is needed.
guard against crashes
wrap the scope with killOnThrow so a crash mid-flow tears down the whole subtree instead of leaving agents running:
await orcr.scope("review", async () => {
// ... same body ...
}, { killOnThrow: true });this flow is a singleton by design: a second concurrent copy fails fast with path_in_use, which is usually what you want. to run several at once, parameterize the root, for example orcr.scope("review_" + prNumber, ...), or use the {rand} placeholder in a path.
next steps
- fan-out and merge and the rest of the patterns cookbook generalize this shape.
- scopes, lineage and fan-out covers path composition in depth.
- schedule work with loops turns a workflow into a recurring job.