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 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.

On this page