---
title: Structured Output
description: How Smithers validates agent outputs against Zod schemas, retries on failure, and handles auto-populated columns.
---

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

## Schema-Driven Output

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

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

```tsx
<Task id="analyze" output={outputs.analysis} agent={analyst} outputSchema={analysisSchema}>
  <AnalysisPrompt repo={ctx.input.repoPath} />
</Task>
```

```mdx
{/* 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 stripping** -- `runId`, `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

| Column | Type | Description |
|---|---|---|
| `runId` | `string` | Current run ID |
| `nodeId` | `string` | Task `id` prop |
| `iteration` | `integer` | Loop 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:

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

```tsx
<Task id="config" output={outputs.config} noRetry>
  {{ environment: "production", version: 3 }}
</Task>
```

Because static payload mismatches are usually deterministic authoring errors, `noRetry` is a good default for one-shot validation. Without it, the normal task retry policy still applies.

## JSON Mode Columns

With `createSmithers`, Zod arrays and objects are automatically stored as JSON text columns:

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

```tsx
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](/guides/error-handling) -- What happens when validation fails after all retries.
- [Patterns](/guides/patterns) -- Schema organization for larger projects.
- [Data Model](/concepts/data-model) -- Required columns and primary key conventions.
