Skip to content

Typed Structured Output

Agents produce prose. Sometimes you need a typed object — an invoice, a parsed entity, a classification result — not a string. Typed structured output gives you that: call .withOutputSchema(schema) in the builder chain and the agent populates result.object as your declared type, while result.output carries the steered JSON answer the agent was guided to emit.

Two engines handle the extraction depending on what your stack supports:

  • Fast — single-shot native JSON enforcement (frontier models with native function calling and no tool overlap). Lowest latency.
  • Grounded — extracts, grounds each field against the tool-result evidence corpus from the run, surgically re-extracts missing required fields, and scores per-field confidence. Enables provenance + abstention. The differentiator for research or data-extraction agents.

The mode is auto-selected by default and can be overridden.

Works across all supported providers and tiers — Anthropic, OpenAI, Gemini, and local Ollama models (qwen, gemma, etc.) — making it a practical option even on self-hosted infrastructure.

.withOutputSchema() is a builder method — call it in the builder chain before .build(), then use agent.run() or agent.streamObject() on the built agent.

import { ReactiveAgents } from "reactive-agents";
import { z } from "zod";
const InvoiceSchema = z.object({
vendor: z.string(),
total: z.number(),
currency: z.string(),
lineItems: z.array(
z.object({ description: z.string(), amount: z.number() })
),
});
const agent = await ReactiveAgents.create()
.withName("invoice-extractor")
.withSystemPrompt("You extract structured data from documents.")
.withTools({ builtins: ["file-read"] })
.withOutputSchema(InvoiceSchema)
.build();
const result = await agent.run(
"Extract the invoice details from invoice.pdf"
);
if (result.object) {
// result.object is typed as z.infer<typeof InvoiceSchema>
console.log(`${result.object.vendor}: ${result.object.total} ${result.object.currency}`);
}

.withOutputSchema() accepts any Standard Schema v1 validator (Zod 3.24+, Valibot, ArkType) or an Effect Schema.Schema<A> imported from "effect" (not "@effect/schema"). The returned builder is re-typed so result.object is A at compile time. Reactive Agents supports all four of these schema libraries out of the box — most frameworks support only one or two.

All four schema libraries support top-level array schemas directly:

import { z } from "zod";
const LineItemList = z.array(
z.object({ description: z.string(), amount: z.number() })
);
const agent = await ReactiveAgents.create()
.withOutputSchema(LineItemList)
.build();
const result = await agent.run("List all line items from the invoice");
// result.object is typed as Array<{ description: string; amount: number }>
if (result.object) {
result.object.forEach((item) => console.log(item.description, item.amount));
}

When .withOutputSchema() is set, the agent is steered to emit JSON matching the schema as its final answer. As a result:

  • result.output is the raw JSON string the agent produced (not prose).
  • result.object is the parsed, typed value derived from that JSON.

If you have existing code that reads result.output as prose, be aware it will contain JSON when structured output is active. Use result.object for the typed value and fall back to result.output only as a last resort (e.g., when result.object is undefined after a parse failure).

By default the agent is lenient: a parse failure does not throw. Instead:

  • result.object is undefined.
  • result.objectError is set to a human-readable description of what went wrong.
const result = await agent.run("Extract invoice details");
if (result.object) {
// happy path
} else if (result.objectError) {
console.error("Extraction failed:", result.objectError);
// fall back to result.output (the raw JSON / text answer)
}

Choose between two failure modes via the onParseFail option:

ModeBehaviour
"degrade" (default)object is undefined, objectError is set. Never throws.
"throw"Throws StructuredOutputError (carries .rawText + .issues).
import { StructuredOutputError } from "reactive-agents";
const strictAgent = await ReactiveAgents.create()
.withOutputSchema(InvoiceSchema, { onParseFail: "throw" })
.build();
try {
const result = await strictAgent.run("Extract invoice details");
console.log(result.object);
} catch (e) {
if (e instanceof StructuredOutputError) {
console.error("Parse failed:", e.issues);
console.log("Raw text was:", e.rawText);
}
}

The grounded path — provenance, confidence, and abstention

Section titled “The grounded path — provenance, confidence, and abstention”

For extraction tasks where you need to know why a value was chosen — or when model hallucination is a concern — use mode: "grounded". Grounded mode is most useful when tools are registered: the engine grounds each field against the actual tool-result evidence corpus the agent accumulated during the run. Without tools there is no evidence corpus, so the grounded path falls back to a best-effort extraction.

The grounded engine:

  1. Extracts fields from the agent’s final answer.
  2. Grounds each field against the tool-result evidence corpus accumulated during the run (the actual data the agent read, not its prose summary).
  3. Scores per-field confidence (0–1).
  4. Surgically re-extracts missing required fields if they were omitted.
const agent = await ReactiveAgents.create()
.withSystemPrompt("You extract financial data from SEC filings.")
.withTools({ builtins: ["web-search", "file-read"] })
.withOutputSchema(InvoiceSchema, { mode: "grounded" })
.build();
const result = await agent.run("Extract Q3 revenue from the attached 10-Q");
if (result.object) {
console.log(result.object.total);
// Per-field evidence trace
console.log(result.provenance?.total);
// → { source: "10-Q page 4", evidence: "Net revenues for Q3 were $..." }
// Per-field confidence (0..1)
console.log(result.confidence?.total); // → 0.97
}

Set abstainBelow to have the grounded engine omit non-required fields whose confidence falls below a threshold, rather than emitting a low-confidence guess:

const agent = await ReactiveAgents.create()
.withOutputSchema(InvoiceSchema, {
mode: "grounded",
abstainBelow: 0.7,
})
.build();
const result = await agent.run("...");
// Fields below 0.7 confidence are omitted from result.object
// and recorded here instead:
console.log(result.abstained);
// → { currency: "confidence 0.42 below abstainBelow threshold" }

abstainBelow is opt-in (off by default) and requires the grounded path.

FieldTypeWhen present
result.objectA | undefinedParse succeeded
result.objectErrorstringParse failed (lenient mode)
result.provenanceRecord<string, { source, evidence }>Grounded path
result.confidenceRecord<string, number>Grounded path
result.abstainedRecord<string, string>Grounded path + abstainBelow set
ValueBehaviour
"auto" (default)fast for frontier models with native JSON + no tool overlap; else grounded.
"fast"Single-shot extraction. No grounding, provenance, or abstention.
"grounded"Loop-integrated extraction with evidence grounding.

Streaming structured output (streamObject)

Section titled “Streaming structured output (streamObject)”

Use streamObject() to receive a DeepPartial<A> as tokens arrive, finishing with the complete validated object:

const agent = await ReactiveAgents.create()
.withOutputSchema(InvoiceSchema)
.build();
for await (const { object } of agent.streamObject("Extract invoice details")) {
// object is DeepPartial<Invoice> — fields appear as the model emits them
if (object.vendor) process.stdout.write(`\rVendor so far: ${object.vendor}`);
}
// Final iteration carries the full validated object

streamObject() throws synchronously if .withOutputSchema() was not called. When onParseFail: "throw" is set, it throws StructuredOutputError at the end if the final buffer fails validation.

Note that runStream() and resumeRun() return the base stream / result and do not carry the typed object; use run() or streamObject() for typed structured output.

.withOutputSchema() composes with the full builder chain, including Compose API killswitches. The agent loop runs under the composed harness; structured extraction fires after the loop completes.

import { ReactiveAgents } from "reactive-agents";
import { budgetLimit } from "@reactive-agents/compose";
import { z } from "zod";
const ReportSchema = z.object({
summary: z.string(),
riskLevel: z.enum(["low", "medium", "high"]),
findings: z.array(z.string()),
});
const agent = await ReactiveAgents.create()
.withSystemPrompt("You are a risk analysis agent.")
.withTools({ builtins: ["web-search", "file-read"] })
.compose(budgetLimit({ maxTokens: 50_000 }))
.withOutputSchema(ReportSchema)
.build();
const result = await agent.run("Analyse the attached contract for risk");
if (result.object) {
console.log(result.object.riskLevel, result.object.findings);
}

Note: structured output is not itself a composable chokepoint — it runs as a post-loop extraction step. The harness governs the reasoning loop; the extraction call (when the parse-first path misses) happens outside harness governance.

  • Lenient by defaultonParseFail: "degrade" means failures are silent unless you check result.objectError. Use "throw" when you want failures to be loud.
  • Grounded field-level features are richest with Effect Schema. Standard Schema inputs (Zod/Valibot/ArkType) get provenance and confidence scoring, but requirement-tracking and surgical re-extraction of missing required fields are Effect-Schema-only today.
  • abstainBelow and grounded-default routing are opt-in pending cross-tier ablation (project lift rule). Auto-mode selects grounded only when the fast path is not applicable.
  • runStream() / resumeRun() results do not carry result.object — use run() or streamObject() for typed output.
  • On slow local models, the extraction adds latency only when the parse-first path misses (the agent is steered to emit JSON, so the common path is a free parse of the model’s own output).
  • result.output is JSON, not prose, when structured output is active. Code relying on result.output as a human-readable string should switch to result.object for the typed value.
  • Reasoning — the kernel loop that populates the evidence corpus the grounded engine draws from.
  • Durable Execution — crash-resume for long extraction runs.
  • Tools — tools produce the evidence that grounded extraction grounds against.