Skip to main content
Workflows are JSX trees. Smithers renders the tree, extracts ready tasks, executes them, persists their outputs, and re-renders. Branching, looping, and parallelism are normal JSX.

Setup

Most projects should use bunx smithers-orchestrator init; it scaffolds everything below. To embed into an existing codebase:
bun add smithers-orchestrator react react-dom zod
bun add -d typescript @types/react @types/react-dom @types/node
Minimal tsconfig.json:
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "smithers-orchestrator",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
jsxImportSource is the only non-standard line; it routes JSX through smithers-orchestrator/jsx-runtime instead of React DOM. Optional MDX prompts: add bun add -d @types/mdx and a preload.ts that calls mdxPlugin(), register it in bunfig.toml as preload = ["./preload.ts"]. Verify with bunx tsc --noEmit and bunx smithers-orchestrator --help.

A minimal workflow

// @jsxImportSource smithers-orchestrator (only needed if not set in tsconfig.json)
import { createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  analysis: z.object({ summary: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="analyze">
    <Sequence>
      <Task id="analyze" output={outputs.analysis}>
        {{ summary: `Analyze ${ctx.input.repo}` }}
      </Task>
    </Sequence>
  </Workflow>
));
outputs.analysis is the typed reference for the Zod schema, so typos are compile errors. The task body is a JSX expression ({...}) whose value is a plain object, with no LLM call here, just a static return. Real tasks pass a run prop or an AI model. See the Task component reference.

Reactivity

The tree re-renders on every frame, so branching is a normal JSX conditional. Inside a workflow function, ctx exposes ctx.input and ctx.outputMaybe(ref, { nodeId }). The latter returns the output of a completed task, or undefined if it hasn’t run yet:
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
{analysis ? <Task id="report" output={outputs.report} agent={writer}>...</Task> : null}
The report Task doesn’t exist in the plan until analysis completes. No placeholder, no skipped node. The conditional IS the dependency. Unlike static DAG tools that require you to declare optional nodes upfront, the JSX conditional is evaluated fresh each frame: if analysis is undefined, the report task simply doesn’t exist in that frame’s plan.
  • Tour: six-step worked example with agents, schemas, approvals, resume.
  • How It Works: the render → execute → persist loop.
  • Components: full prop surface for every JSX element.