Little documentation
Guides and APICookbook

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 of asHarnessWorkflow + createHarness({ workflows }); see Compose With Little Harness. The replacement covers bounded fan-out: workflowBudgets.maxConcurrentWorkflowRuns / maxQueuedWorkflowRuns cap how many workflow tool calls run at once, as maxConcurrentSubRuns did. It does not yet cover planning once. A Harness workflow tool plans and runs inside a single tool call, so there is no workflowVersionId to hold and reuse across N inputs. The protocol reserves the seam: HarnessWorkflow declares optional prepareForHarness / runPreparedForHarness, and HarnessPreparedWorkflow carries a versionId. 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-time DeprecationWarning (code LWF_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

On this page