Documentation Index
Fetch the complete documentation index at: https://smithers.sh/llms.txt
Use this file to discover all available pages before exploring further.
A code-review workflow built one capability at a time. Each step is a diff against the previous. Reading time: 15 minutes.
1. Install and scaffold
bunx smithers-orchestrator init
bun add smithers-orchestrator ai @ai-sdk/anthropic zod
bun add -d typescript @types/bun
export ANTHROPIC_API_KEY="sk-ant-..."
init creates .smithers/ with seeded workflows, prompts, and components. The bun deps add the AI SDK, Anthropic provider, and Zod (schemas).
A 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 line specific to Smithers — it routes JSX through the workflow runtime instead of React DOM.
2. One-task workflow
/** @jsxImportSource smithers-orchestrator */
import { createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";
const { Workflow, smithers, outputs } = createSmithers({
greeting: z.object({ message: z.string() }),
});
export default smithers((ctx) => (
<Workflow name="hello">
<Sequence>
<Task id="greet" output={outputs.greeting}>
{{ message: `Hello, ${ctx.input.name}` }}
</Task>
</Sequence>
</Workflow>
));
createSmithers registers Zod schemas; each becomes a SQLite table. outputs.greeting is the typed reference for the greeting schema — using it as the output prop gives compile-time checks (typo outputs.greting is a type error).
This Task has no agent, just a literal value. Run it.
bunx smithers-orchestrator up workflow.tsx --input '{"name":"world"}'
Inspect:
bunx smithers-orchestrator ps # find the run id
bunx smithers-orchestrator inspect <run-id> # structured state
sqlite3 smithers.db "SELECT * FROM greeting;" # the persisted output
3. Add an agent task
Replace the literal Task with an agent Task whose output is structured.
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
const { Workflow, smithers, outputs } = createSmithers({
analysis: z.object({
summary: z.string(),
issues: z.array(z.object({
file: z.string(),
line: z.number(),
severity: z.enum(["low", "medium", "high"]),
description: z.string(),
})),
}),
});
const analyst = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
instructions: "You are a senior code reviewer. Return structured JSON.",
});
export default smithers((ctx) => (
<Workflow name="review">
<Sequence>
<Task id="analyze" output={outputs.analysis} agent={analyst}>
{`Review the code in ${ctx.input.repo} and return analysis as JSON.`}
</Task>
</Sequence>
</Workflow>
));
The runtime injects a JSON-schema description of outputs.analysis into the prompt, parses the agent’s response, validates against Zod, and persists. Validation failure triggers a retry.
4. A second task that depends on the first
Tasks see each other’s outputs through ctx.outputMaybe(...). An incomplete upstream returns undefined; on the next render frame the upstream output appears and the downstream Task mounts.
const { Workflow, smithers, outputs } = createSmithers({
analysis: AnalysisSchema,
fix: z.object({
patch: z.string(),
filesChanged: z.array(z.string()),
}),
});
const fixer = new Agent({
model: anthropic("claude-sonnet-4-20250514"),
instructions: "Write minimal, correct fixes as a unified diff.",
});
export default smithers((ctx) => {
const analysis = ctx.outputMaybe(outputs.analysis, { nodeId: "analyze" });
return (
<Workflow name="review">
<Sequence>
<Task id="analyze" output={outputs.analysis} agent={analyst}>
{`Review ${ctx.input.repo}`}
</Task>
{analysis ? (
<Task id="fix" output={outputs.fix} agent={fixer}>
{`Fix these issues:\n${analysis.issues.map(i =>
`- [${i.severity}] ${i.file}:${i.line} — ${i.description}`
).join("\n")}`}
</Task>
) : null}
</Sequence>
</Workflow>
);
});
Render 1: only analyze is mounted. Render 2 (after analyze finishes): analysis is populated, fix mounts and runs. The entire reactivity story — no hooks, no subscriptions, JSX conditionals over persisted state.
Same shape works for branching, parallel groups, and loops:
<Parallel maxConcurrency={3}>
<Task id="lint" output={outputs.lint} agent={linter}>...</Task>
<Task id="test" output={outputs.test} agent={tester}>...</Task>
<Task id="audit" output={outputs.audit} agent={auditor}>...</Task>
</Parallel>
<Loop until={!!review?.approved} maxIterations={5}>
<Task id="implement" output={outputs.impl} agent={implementer}>...</Task>
<Task id="review" output={outputs.review} agent={reviewer}>...</Task>
</Loop>
5. An approval gate
Pause for a human. The runtime persists the pending decision and exits cleanly; an operator approves or denies through the CLI; resume picks up from the gate.
import { Approval } from "smithers-orchestrator";
{analysis ? (
<Approval
id="confirm-fix"
output={outputs.confirmFix}
request={{
title: `Apply fixes for ${analysis.issues.length} issues?`,
summary: analysis.summary,
}}
onDeny="skip"
>
{/* children rendered after approval */}
</Approval>
) : null}
{ctx.outputMaybe(outputs.confirmFix, { nodeId: "confirm-fix" })?.approved ? (
<Task id="fix" output={outputs.fix} agent={fixer}>
{`Apply patches`}
</Task>
) : null}
Operator side:
bunx smithers-orchestrator ps --status waiting-approval # find paused runs
bunx smithers-orchestrator inspect <run-id> # see the request
bunx smithers-orchestrator approve <run-id> --node confirm-fix --by alice
bunx smithers-orchestrator up workflow.tsx --run-id <run-id> --resume true
onDeny controls behavior on rejection: "fail" aborts the run, "continue" proceeds without the approved branch, "skip" skips the gated tasks.
6. Crash, then resume
Every completed task’s output sits in SQLite. A crash, kill, or restart loses no work — the next run with --resume true skips finished tasks.
bunx smithers-orchestrator up workflow.tsx --input '{"repo":"."}' --run-id review-1
# ...analyze finishes, fix is mid-flight, you Ctrl+C
bunx smithers-orchestrator up workflow.tsx --run-id review-1 --resume true
# analyze is skipped (already in DB), fix re-runs from scratch (was incomplete)
In-flight attempts are marked stale and re-tried; finished tasks are not. Resume is deterministic — same input + same code = same task IDs.
For unattended recovery, run the supervisor:
bunx smithers-orchestrator supervise --interval 30s --stale-threshold 1m
It auto-resumes runs whose owner process died.
What you skipped (and where to find it)
Read next
- How It Works — the render → execute → persist loop.
- Components — JSX surface reference.
- CLI — every command in one TOON catalog.
- Recipes — patterns from production workflows.