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.
API reference: Authoring and Components list every authoring helper and JSX element, their options, and links to source and tests.

Setup

Most projects should use bunx smithers-orchestrator init; it scaffolds everything below. To embed into an existing codebase:
bun add smithers-orchestrator zod
bun add -d typescript @types/react @types/node
smithers-orchestrator bundles React and ships the JSX runtime as smithers-orchestrator/jsx-runtime, so you do not add react or react-dom yourself. jsxImportSource (below) routes JSX through the workflow reconciler, never React DOM. The one required dev dep is @types/react (React ships no bundled types, and the JSX transform resolves its JSX namespace from it). The browser UI surface (smithers-orchestrator/gateway-react) is the only place that takes react/react-dom as peers. 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({
  input: z.object({ repo: z.string() }),
  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>
));
createSmithers is a named export. The lowercase smithers wrapper is not a top-level import; it is the property returned by createSmithers(...). smithers((ctx) => ...) returns the SmithersWorkflow value to export from the workflow file. The input schema above types ctx.input.repo; omit input and ctx.input is unknown, so you must guard or parse it yourself. 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: The ctx parameter is optional: it is just a normal function argument, so a workflow that never reads ctx.input or ctx.outputMaybe can drop it entirely and write smithers(() => ( ... )). Add (ctx) only when you actually reference dynamic input or prior task output.
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.