generate-and-filter
fan the same prompt across several providers and models, judge the drafts once, and keep the winner with a safe fallback.
send one prompt to several generators at once, each a different provider or model, then judge the drafts with a single agent and keep the best. this is the parallel shape aimed at quality: you spend on breadth up front and filter down to one answer.
the scenario
for open-ended work, one model's first draft is rarely the best you can get. fan the same prompt across a mix of providers and models, collect every draft in parallel, then have one judge pick the winner. you pay for N drafts plus one judgment, and you get the benefit of different models' strengths on the same task.
the annotated recipe
import { orcr } from "@orchestratr/sdk";
const GENERATORS = [
{ agent: "claude", model: "opus" },
{ agent: "claude", model: "sonnet" },
{ agent: "codex" },
];
await orcr.scope("landing_copy", async () => {
const drafts = await Promise.all(GENERATORS.map((g, i) =>
orcr.ask({ ...g, path: `generate/gen_${i}`,
prompt: "Write hero copy for orchestratr.dev: one headline, one subhead." })));
const pick = await orcr.ask({
agent: "claude", path: "judge/picker",
prompt: `Pick the best draft. Reply with only its number.\n` +
drafts.map((d, i) => `--- ${i} ---\n${d}`).join("\n"),
});
console.log(drafts[parseInt(pick.trim(), 10)] ?? drafts[0]);
});what each piece is doing:
GENERATORSis the roster: a provider, and optionally amodeloreffortoverride per entry. here two claude models (opus,sonnet) and one codex.{ ...g }spreads each entry'sagentandmodelinto theaskcall.- the fan-out is
Promise.alloverGENERATORS.map(...). all drafts run concurrently, each at a distinct path (landing_copy/generate/gen_0,gen_1,gen_2). - the judge is a single
orcr.ask. it receives every draft, numbered, and is told to reply with only the number. one judgment over the whole set is cheaper and more consistent than pairwise comparisons. - the fallback is the last line.
drafts[parseInt(pick.trim(), 10)] ?? drafts[0]parses the judge's answer as an index; if that answer is not a parseable number in range,parseIntyieldsNaN, the index misses, and?? drafts[0]falls back to the first draft. the workflow always returns a real draft.
model routing lives in the roster
agent, model, and effort are the routing knobs. to try a new model, add one line to GENERATORS. the judge is deliberately a single fixed provider so the comparison is consistent across runs. see choose providers & models.
primitives it exercises
orcr.scopeto root the generators and the judge.- parallel
orcr.askacross a roster, each with a per-entrymodeloverride. - provider and model routing via the spread
{ ...g }. - a single judge
askover the collected drafts.
failure and cleanup
- unparseable judge output falls back to the first draft. the
?? drafts[0]guard means a judge that replies "I like the second one" instead of "1" cannot crash the workflow; it just loses its vote. this is the same principle as classify-and-act: never trust the shape of model output. - every generator and the judge are
gc: "immediate"(viaask), so each pane closes as its response is captured. no parked panes. - if a generator is
blockedor its transcript is unreadable,orcr.askthrows. add{ killOnThrow: true }to the scope to tear down the rest of the batch on a throw.
run it
orcr scaffold copy-workflow && cd copy-workflow
# paste the recipe into workflow.ts, replacing the generated example
npx tsx workflow.tsevery provider in the roster needs both integration layers installed, or the run fails fast with integration_missing. see providers & integrations.
next: tournament is what to reach for when the roster grows too large to judge in one prompt.