Plan once, run many
An orchestrator plans a workflow once and fans it out across many inputs in parallel.
Little Workflow: published on npm as little-workflow@0.2.0-alpha.1 (2026-10-06). Sections labeled UNRELEASED describe source-only changes. Release status
Deprecated surface, and the gap that keeps it alive. This recipe runs on the orchestrator (
plan_workflow+run_workflow), which is being retired in favour ofasHarnessWorkflow+createHarness({ workflows }); see Compose With Little Harness. The replacement covers bounded fan-out:workflowBudgets.maxConcurrentWorkflowRuns/maxQueuedWorkflowRunscap how many workflow tool calls run at once, asmaxConcurrentSubRunsdid. It does not yet cover planning once. A Harness workflow tool plans and runs inside a single tool call, so there is noworkflowVersionIdto hold and reuse across N inputs. The protocol reserves the seam:HarnessWorkflowdeclares optionalprepareForHarness/runPreparedForHarness, andHarnessPreparedWorkflowcarries aversionId. Neither side implements it today. Until it lands, plan once, run many stays on the orchestrator. It still works, and its entry points emit a one-timeDeprecationWarning(codeLWF_DEP_ORCHESTRATOR).
When you have one workflow shape and many inputs — score every candidate, analyze every company, process every ticket — you do not want to re-plan for each input. The Orchestrator role plans the sub-workflow once and runs it across N inputs in parallel, up to maxConcurrentSubRuns.
This recipe is lifted from demos/orchestrator-fanout, which reviews 30 candidates through one reusable workflow.
The reusable sub-workflow
Author one workflow that handles a single input. defineWorkflow takes the full config (model slots, planner, worker harness). The demo passes createWorkflowHarness() explicitly as worker.harness, which is the same default worker that executes its tool.call steps. That worker does not execute code.run steps, so the demo's plans use tool.call only.
import {
createToolRegistry,
defineWorkflow,
localWorld,
model,
output,
runOrchestration,
} from "@little-stack/workflow";
import { createWorkflowHarness } from "@little-stack/workflow/internals";
import { tool } from "ai";
const workflow = defineWorkflow({
id: "candidate.review",
description: "Review one candidate.",
input: candidateInputSchema,
output: output.object({ schema: candidateOutputSchema }),
models: [model({ provider: "demo", modelId: "worker" }, { id: "model.worker" })],
planner: { model: { provider: "demo", modelId: "planner" }, harness: plannerHarness },
tools: ["emitCandidate"],
worker: { harness: createWorkflowHarness() },
});
const tools = createToolRegistry({
emitCandidate: tool({
description: "Return the candidate review payload.",
inputSchema: candidateInputSchema,
outputSchema: candidateOutputSchema,
execute: async ({ candidateId }) => ({ candidateId, label: `reviewed:${candidateId}` }),
}),
});Fan out with the Orchestrator
Pass the workflow in an array to runOrchestration with an orchestrator config. That runs an orchestrator session instead of a single planner run; its plan_workflow / run_workflow tools plan the shape once and reuse the compiled WorkflowVersion across every sub-run.
const result = await runOrchestration({
world: localWorld({ dataDir: ".little-workflow-demo" }),
workflows: [workflow],
orchestrator: {
model: { provider: "demo", modelId: "orchestrator" },
harness: orchestratorHarness,
},
input: { batch: "demo" },
tools,
maxConcurrentSubRuns: 10,
});runOrchestration requires an orchestrator config, and maxConcurrentSubRuns (default 10) caps how many sub-runs execute at once. The demo uses deterministic stub harnesses so it runs keyless; in production the planner/orchestrator/worker harnesses are model-backed. See Orchestrating Workflows for the full role model.
Run it
pnpm --filter orchestrator-fanout demo