Structured output

Get a typed, validated value back from a run instead of free text.

Pass a schema as output (a Zod schema in TypeScript, a Pydantic model class in Python) and a completed run's output is a value of that type. The model returns its answer through a final_output tool whose input is your schema. threads validates each answer strictly ("1" is not a number); if one fails, the model sees the validation error and tries again.

const Triage = z.object({
  severity: z.enum(["low", "medium", "high"]),
  summary: z.string(),
  needsHuman: z.boolean(),
});

const triage = agent({
  instructions: "Triage the incoming support ticket.",
  model: scriptedModel({
    responses: [
      {
        content: [{ type: "tool_use", call_id: "c1", name: "final_output", input: { severity: "urgent", summary: "x", needsHuman: true } }],
        stop_reason: "tool_use",
        usage,
      },
      {
        content: [{ type: "tool_use", call_id: "c2", name: "final_output", input: { severity: "high", summary: "Checkout is down", needsHuman: true } }],
        stop_reason: "tool_use",
        usage,
      },
    ],
  }),
  output: Triage,
  outputRetries: 2,
});

const result = await triage.run("Nobody can pay since 9am!", { store: sqlite(":memory:") });
if (result.status === "completed") {
  const { severity, needsHuman } = result.output; // typed as z.infer<typeof Triage>
  console.log(severity, needsHuman);
}

The scripted model's first answer uses "urgent", which isn't allowed, so it is rejected and the second answer is accepted. With a real model you only write the schema.

Options

outputz.ZodType / type[BaseModel]

The schema of the final answer. The agent's type becomes Agent<Deps, z.infer<typeof schema>> in TypeScript and Agent[D, Triage] in Python. In Python anything but a Pydantic model class is a ConfigError.

outputRetries / output_retriesnumber / intdefault 2

When this many answers have failed in a turn (rejected by the schema, or plain text with no final_output call, which is answered with a request to call it), the run ends failed with code output_invalid. An integer from 0 to 2^53 − 1; anything else is a ConfigError (invalid_config).

What an output schema can use

The log checks every accepted answer against the schema itself, the same way in both languages. An answer your validator takes but the log's check doesn't (a datetime without a time zone, a URL with a space) is rejected like any other, and the model tries again. What the check covers:

In the schemaFrom
Types, required and optional fields, Literals and enums, unions, nested and recursive modelsany model
Bounds (ge, gt, le, lt) and multiple_of / .multipleOf(), exact in decimalnumbers
Lengths, and patterns in a portable regex subset (ASCII \d \w \s, no named groups, lookbehind or \p{...})strings and lists
date-time (RFC 3339, with a time zone), date, time (with a time zone), email, uri, uuiddatetime, date, time, UUID, EmailStr, AnyUrl; z.iso.datetime({ offset: true }), z.iso.date(), z.email(), z.url(), z.uuid()

Pydantic's EmailStr needs its optional email validator: pip install email-validator (threads doesn't install it). A plain str with Field(json_schema_extra={"format": "email"}) gets the same check from the log without it.

Anything else, such as a tuple (prefixItems), a dict with constrained keys or another format, is refused when the agent is set up, with a ConfigError (invalid_config) that names the keyword. The exact rules are in the log schema reference.

Each accepted or rejected answer is recorded in the log, so you can see in the timeline exactly what the model tried.

Subagents

A subagent with an output hands its parent the accepted value as canonical JSON text (sorted keys, no spaces). That text is the spawn_agent call's result and the child's recorded output, and it is the same bytes in both languages.

Edit on GitHub

On this page