orchestratr
patterns

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:

  • GENERATORS is the roster: a provider, and optionally a model or effort override per entry. here two claude models (opus, sonnet) and one codex. { ...g } spreads each entry's agent and model into the ask call.
  • the fan-out is Promise.all over GENERATORS.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, parseInt yields NaN, 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.scope to root the generators and the judge.
  • parallel orcr.ask across a roster, each with a per-entry model override.
  • provider and model routing via the spread { ...g }.
  • a single judge ask over 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" (via ask), so each pane closes as its response is captured. no parked panes.
  • if a generator is blocked or its transcript is unreadable, orcr.ask throws. 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.ts

every provider in the roster must be built into orcr and have its Herdr adapter 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.

On this page