Skip to main content
This tutorial walks you through building a complete, production-ready workflow from scratch. You will define schemas with createSmithers, configure AI agents, compose tasks in a <Sequence>, read previous outputs through ctx, and inspect the results. By the end, you will have a three-step code review workflow: analyze a codebase, generate a fix, then produce a summary report.

Step 1: Project Setup

Create a new directory and install dependencies:
mkdir code-review && cd code-review
bun init -y
bun add smithers-orchestrator ai @ai-sdk/anthropic zod
Configure TypeScript for Smithers JSX:
// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "smithers-orchestrator",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
Set your API key:
export ANTHROPIC_API_KEY="sk-ant-..."
Your project structure will look like this:
code-review/
  tsconfig.json
  package.json
  workflow.tsx      # Workflow definition
  main.ts          # Runner (optional -- you can also use the CLI)

Step 2: Define Schemas

createSmithers accepts a record of Zod schemas. Each schema becomes a named output table that Smithers auto-creates in SQLite. You never write SQL or Drizzle table definitions by hand.
// workflow.tsx
import { createSmithers, Task, Sequence } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, useCtx } = 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(),
    })),
  }),
  fix: z.object({
    patch: z.string(),
    explanation: z.string(),
    filesChanged: z.array(z.string()),
  }),
  report: z.object({
    title: z.string(),
    body: z.string(),
    issueCount: z.number(),
    fixedCount: z.number(),
  }),
});
Three things happened here:
  1. analysis, fix, and report are now registered output names. You reference them as strings in <Task output="analysis">.
  2. SQLite tables are auto-created with the correct columns, plus auto-populated runId, nodeId, and iteration columns.
  3. Workflow and smithers are returned pre-configured for this schema set.

Step 3: Configure Agents

Define your AI agents using the Vercel AI SDK. You can use any provider, but this example uses Anthropic:
// workflow.tsx (continued)
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

const analyst = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  instructions: "You are a senior code reviewer. Analyze code for bugs, security issues, and quality problems. Return structured JSON.",
});

const fixer = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  instructions: "You are a senior engineer who writes minimal, correct fixes. Return structured JSON with a unified diff patch.",
});

const reporter = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  instructions: "You are a technical writer. Summarize code review findings into a clear report. Return structured JSON.",
});

Step 4: Build the Workflow

Now compose the tasks. Tasks inside <Workflow> run sequentially by default (top to bottom). You can also use <Sequence> explicitly for clarity:
// workflow.tsx (continued)
export default smithers((ctx) => {
  // Read the analysis output (undefined until the task completes)
  const analysis = ctx.outputMaybe("analysis", { nodeId: "analyze" });
  // Read the fix output
  const fix = ctx.outputMaybe("fix", { nodeId: "fix" });

  return (
    <Workflow name="code-review">
      <Sequence>
        {/* Step 1: Analyze the codebase */}
        <Task id="analyze" output="analysis" agent={analyst}>
          {`Review this code for bugs and issues:

Repository: ${ctx.input.repo}
Focus area: ${ctx.input.focusArea ?? "general"}

Return JSON with:
- summary (string): overall assessment
- issues (array): each with file, line, severity, and description`}
        </Task>

        {/* Step 2: Generate a fix (only renders after analysis completes) */}
        {analysis ? (
          <Task id="fix" output="fix" agent={fixer}>
            {`Fix these issues:

${analysis.issues.map((i) => `- [${i.severity}] ${i.file}:${i.line} - ${i.description}`).join("\n")}

Return JSON with:
- patch (string): unified diff
- explanation (string): what you changed and why
- filesChanged (string[]): list of modified files`}
          </Task>
        ) : null}

        {/* Step 3: Generate a summary report */}
        {fix ? (
          <Task id="report" output="report" agent={reporter}>
            {`Write a code review report.

Analysis summary: ${analysis!.summary}
Issues found: ${analysis!.issues.length}
Fix explanation: ${fix.explanation}
Files changed: ${fix.filesChanged.join(", ")}

Return JSON with:
- title (string)
- body (string): markdown report
- issueCount (number)
- fixedCount (number)`}
          </Task>
        ) : null}
      </Sequence>
    </Workflow>
  );
});
Key points about this workflow:
  • ctx.outputMaybe() returns undefined if the task has not completed yet. This is safe for conditional rendering.
  • Conditional rendering ({analysis ? <Task ... /> : null}) controls task visibility. The fix task only mounts after analyze finishes and provides output.
  • ctx.input contains the input object you pass at run time. Here we expect { repo: string, focusArea?: string }.
  • Output keys like "analysis" match the schema names passed to createSmithers.

Step 5: Create the Runner

You can run the workflow programmatically or via the CLI. Here is the programmatic approach:
// main.ts
import { runWorkflow } from "smithers-orchestrator";
import workflow from "./workflow";

const result = await runWorkflow(workflow, {
  input: { repo: "/path/to/my-project", focusArea: "authentication" },
  onProgress: (event) => {
    if (event.type === "NodeStarted") {
      console.log(`Starting: ${event.nodeId}`);
    }
    if (event.type === "NodeFinished") {
      console.log(`Finished: ${event.nodeId}`);
    }
  },
});

console.log("Status:", result.status);
console.log("Run ID:", result.runId);

if (result.status === "finished") {
  console.log("Report:", JSON.stringify(result.output, null, 2));
}

Step 6: Run It

bun run main.ts
Or use the CLI directly (no main.ts required):
bunx smithers run workflow.tsx --input '{"repo": "/path/to/my-project", "focusArea": "authentication"}'
You should see progress events followed by a result:
Starting: analyze
Finished: analyze
Starting: fix
Finished: fix
Starting: report
Finished: report
Status: finished
Run ID: smth_a1b2c3d4

Step 7: Inspect Results

Use the CLI to inspect the completed run:
# View run status and node states
bunx smithers status workflow.tsx --run-id smth_a1b2c3d4

# View the execution graph
bunx smithers graph workflow.tsx --run-id smth_a1b2c3d4

# View render frames (useful for debugging re-render cycles)
bunx smithers frames workflow.tsx --run-id smth_a1b2c3d4 --tail 5
The SQLite database (smithers.db) contains the full execution history. You can also query it directly:
sqlite3 smithers.db "SELECT * FROM analysis WHERE run_id = 'smth_a1b2c3d4';"
sqlite3 smithers.db "SELECT * FROM report WHERE run_id = 'smth_a1b2c3d4';"

What Happened

Here is the execution flow:
  1. Render 1: Smithers renders the JSX tree. Only analyze is mounted (the other tasks are guarded by null checks). The engine executes analyze.
  2. Render 2: ctx.outputMaybe("analysis", ...) now returns the persisted row. The fix task mounts. The engine executes fix.
  3. Render 3: Both analysis and fix are available. The report task mounts. The engine executes report.
  4. Render 4: All tasks are finished. No runnable tasks remain. The run completes with status "finished".
If the process had crashed at any point (say, during step 2), you could resume:
bunx smithers resume workflow.tsx --run-id smth_a1b2c3d4
The engine would detect that analyze already has a valid output row, skip it, and continue from fix.

Adding Tools

To give your agents access to the filesystem, add built-in tools:
import { read, grep, bash } from "smithers-orchestrator/tools";

const analyst = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  instructions: "You are a senior code reviewer.",
  tools: { read, grep, bash },
});
Tools are sandboxed to the workflow root directory by default. See Built-in Tools for configuration options.

Next Steps

  • Structured Output — How schema validation works in depth.
  • Error Handling — Add retries, timeouts, and fallback paths.
  • Resumability — Understand crash recovery and deterministic replay.
  • Patterns — Recommended project structure for larger workflows.
  • Components — Reference for all JSX components.