Skip to main content
Every <Task> produces structured output validated against a schema and persisted to SQLite.

Schema-Driven Output

import { createSmithers, Task } from "smithers-orchestrator";
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const { Workflow, smithers, outputs } = createSmithers({
  analysis: z.object({
    summary: z.string(),
    issues: z.array(z.string()),
    risk: z.enum(["low", "medium", "high"]),
  }),
});

const analyst = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  instructions: "Return JSON matching the schema exactly.",
});

export default smithers((ctx) => (
  <Workflow name="structured-output">
    <Task id="analyze" output={outputs.analysis} agent={analyst}>
      {`Analyze this codebase: ${ctx.input.target}.
Return JSON with:
- summary (string)
- issues (string[])
- risk ("low" | "medium" | "high")`}
    </Task>
  </Workflow>
));
Downstream tasks consume structured output via deps:
<Task id="report" output={outputs.report} agent={writer} deps={{ analyze: outputs.analysis }}>
  {(deps) => `Write a report for ${deps.analyze.summary}`}
</Task>

The outputSchema Prop

When a <Task> child is a React or MDX element, Smithers auto-injects a schema prop — a JSON example derived from the Zod schema:
<Task id="analyze" output={outputs.analysis} agent={analyst} outputSchema={analysisSchema}>
  <AnalysisPrompt repo={ctx.input.repoPath} />
</Task>
{/* prompts/analysis.mdx */}
Analyze the repository at {props.repo}.

Return JSON matching this schema:
{props.schema}
For string children, describe the expected shape in the prompt text. The outputSchema prop still participates in validation and cache key computation.

Validation Flow

  1. JSON extraction — Tries structured output, raw JSON, code-fenced JSON, then balanced-brace extraction. If none found, a follow-up prompt requests the JSON.
  2. Auto-populated column strippingrunId, nodeId, iteration are stripped before validation. The agent need not include them.
  3. Schema validation — Extracted JSON is validated against Zod schema (if set) and Drizzle table schema.
  4. Auto-retry — On failure, up to 2 retry prompts with Zod error details:
    Your previous response did not match the expected schema.
    Errors:
    - issues: Expected array, received string
    - risk: Invalid enum value. Expected 'low' | 'medium' | 'high', received 'moderate'
    
    Please return valid JSON matching the schema.
    
  5. Persistence — On success, the row is written with runId, nodeId, iteration auto-populated.

Auto-Populated Columns

ColumnTypeDescription
runIdstringCurrent run ID
nodeIdstringTask id prop
iterationintegerLoop iteration (0 for non-loop tasks)
These are auto-added by createSmithers, stripped from agent responses, and auto-populated on write. Zod schemas should only describe business fields:
const analysisSchema = z.object({
  summary: z.string(),
  issues: z.array(z.string()),
});
// Agent returns: { "summary": "...", "issues": ["..."] }
// Smithers adds runId, nodeId, iteration automatically.

Static Mode

Tasks without an agent prop write children directly to the database, still validated against the table schema:
<Task id="config" output={outputs.config}>
  {{ environment: "production", version: 3 }}
</Task>
Mismatched payloads fail immediately without retries.

JSON Mode Columns

With createSmithers, Zod arrays and objects are automatically stored as JSON text columns:
const { Workflow, smithers, outputs } = createSmithers({
  analysis: z.object({
    issues: z.array(z.string()), // stored as JSON text automatically
  }),
});

Combining Zod and Drizzle Schemas

With the manual Drizzle API (without createSmithers), pair a Drizzle table with a Zod outputSchema for double validation:
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core";

const analysisTable = sqliteTable(
  "analysis",
  {
    runId: text("run_id").notNull(),
    nodeId: text("node_id").notNull(),
    summary: text("summary").notNull(),
    issues: text("issues", { mode: "json" }).$type<string[]>(),
    risk: integer("risk").notNull(),
  },
  (t) => ({
    pk: primaryKey({ columns: [t.runId, t.nodeId] }),
  }),
);

const analysisSchema = z.object({
  summary: z.string(),
  issues: z.array(z.string()),
  risk: z.number().int().min(1).max(10),
});

<Task id="analyze" output={analysisTable} outputSchema={analysisSchema} agent={analyst}>
  Analyze the codebase.
</Task>
outputSchema validates JSON structure (including the risk range); the Drizzle table validates column types and nullability.

Next Steps

  • Error Handling — What happens when validation fails after all retries.
  • Patterns — Schema organization for larger projects.
  • Data Model — Required columns and primary key conventions.