---
title: Tour
description: Build a code-review workflow in six steps. Every core feature shows up.
---

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

```bash
bunx smithers-orchestrator init
bun add smithers-orchestrator zod@^4
bun add -d typescript @types/bun
codex login
```

`init` creates `.smithers/` with seeded workflows, prompts, and components. The
bun deps add Smithers and Zod (schemas); `codex login` makes your Codex
subscription available to workflow workers.

<Note>
**Zod v4 is required.** Smithers introspects your output schemas via Zod v4
internals, so pin `zod@^4`. A Zod **v3** schema fails when building an agent
command with the cryptic error `undefined is not an object (evaluating
'schema._zod.def')`.
</Note>

A minimal `tsconfig.json`:

```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

```tsx
/** @jsxImportSource smithers-orchestrator */
import { createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  input: z.object({ name: z.string() }), // types ctx.input
  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 durable output relation managed by the runtime. `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).

The `input` key is special: its schema types `ctx.input` (so `ctx.input.name` is a checked `string`, not `unknown`). Every other key is an output table. Omit `input` and `ctx.input` is untyped, forcing a defensive guard on each field (`ctx.input?.name ?? "world"`). Input fields also arrive as supplied, with no Zod defaults applied, so coalesce any field you do not require. The other schemas (`greeting` here) are the workflow's outputs.

This Task has no `agent`, just a literal value. Run it.

```bash
bunx smithers-orchestrator up workflow.tsx --input '{"name":"world"}'
```

Inspect:

```bash
bunx smithers-orchestrator ps                  # find the run id
bunx smithers-orchestrator inspect RUN_ID    # structured state
bunx smithers-orchestrator output RUN_ID greet --pretty  # typed node output
```

For a controller or custom monitor, read the same output through Gateway
`getNodeOutput({ runId, nodeId: "greet" })`. Do not query the backing store;
SQLite/PGlite/Postgres are interchangeable runtime details.

## 3. Add an agent task

Replace the literal Task with an agent Task whose output is structured.

```tsx
/** @jsxImportSource smithers-orchestrator */
import { CodexAgent, 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(),
    issues: z.array(z.object({
      file: z.string(),
      line: z.number(),
      severity: z.enum(["low", "medium", "high"]),
      description: z.string(),
    })),
  }),
});

const analyst = new CodexAgent({
  model: "gpt-5.6-sol",
  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.

For the common case of one Task consuming exactly one upstream output, `<Task deps={{ analyze: outputs.analysis }}>` with a `(deps) => ...` children callback is the more ergonomic form; reach for `ctx.outputMaybe` when the guard needs to inspect content or gate more than one sibling.

```tsx
/** @jsxImportSource smithers-orchestrator */
import { CodexAgent, createSmithers, Sequence, Task } from "smithers-orchestrator";
import { z } from "zod";

const AnalysisSchema = 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 { Workflow, smithers, outputs } = createSmithers({
  input: z.object({ repo: z.string() }),
  analysis: AnalysisSchema,
  fix: z.object({
    patch: z.string(),
    filesChanged: z.array(z.string()),
  }),
});

const analyst = new CodexAgent({
  model: "gpt-5.6-sol",
  instructions: "You are a senior code reviewer. Return structured JSON.",
});

const fixer = new CodexAgent({
  model: "gpt-5.6-luna",
  config: { model_reasoning_effort: "medium" },
  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. That is the entire reactivity story: no hooks, no subscriptions, JSX conditionals over persisted state.

Same shape works for branching, parallel groups, and loops. A `?:` conditional
is the inline form; [`<Branch>`](/components/branch) is the declarative form when
you want explicit `then`/`else` (it takes those as props, not children):

```tsx
<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>

<Branch
  if={!!analysis && analysis.issues.length > 0}
  then={<Task id="fix"  output={outputs.fix}  agent={fixer}>...</Task>}
  else={<Task id="ship" output={outputs.ship} agent={shipper}>...</Task>}
/>

<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; the operating agent relays the question to the human, then approves or denies through the CLI; resume picks up from the gate.

```tsx
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 (you, the agent, run these on the human's behalf; never hand them to the human):

```bash
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.

```bash
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)
```

<Frame caption="The same crash-and-resume mechanic: a run is killed mid-task, then resuming skips the finished work and re-runs only the interrupted task from its last persisted frame.">
  <img src="/images/why/crash-resume.gif" alt="A Smithers run is killed partway through, then resumes: the completed task is skipped, the in-flight task re-runs as a new attempt, and the run finishes" />
</Frame>

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:

```bash
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)

- **Time travel** (replay a frame, fork a run, diff two runs): `bunx smithers-orchestrator replay|fork|diff|timeline`. See [How It Works → Time travel](/how-it-works#time-travel).
- **Scorers** (attach evaluators to Tasks): see [Recipes → Scoring tasks](/recipes#scoring-tasks).
- **Memory** (cross-run facts and message history): see [How It Works → Memory](/how-it-works#memory-cross-run-state).
- **RAG**, **voice**, **OpenAPI tools**: opt-in fragments. See the index in [llms.txt](/llms.txt).
- **Tool sandbox** (read/grep/bash with path containment): see [Recipes → Tools](/recipes#coherent-task-with-tools).

## Read next

- [How It Works](/how-it-works): the render → execute → persist loop.
- [Components](/components/workflow): JSX surface reference.
- [CLI](/cli/overview): every command in one table.
- [Recipes](/recipes): patterns from production workflows.
