Models and Structured OutputStructured output, and the chronicler gap
No narration yet
Module 1, Lesson 225 min

Structured output, and the chronicler gap

Structured output is the interesting part. You do not want prose back, you want a typed object that matches a schema. Define the schema in zod, then bind it to the model.

Structured output with withStructuredOutput
import * as z from "zod/v4";

const Decision = z.object({
  messageId: z.string(),
  category: z.enum(["A", "B", "C"]),
  confidence: z.enum(["low", "medium", "high"]),
  reason: z.string().optional(),
});

const structured = model.withStructuredOutput(Decision);
const result = await structured.invoke(messages); // typed as Decision

Under the hood, withStructuredOutput uses the provider's tool-calling or structured-output mechanism to force the model to return JSON that fits the schema, then parses it for you. That parse-and-validate step is exactly what chronicler hand-rolls today.

withStructuredOutput should still exist in the 1.5.x line; verify the exact surface at build time, since the modern alternative inside an agent loop is toolStrategy, which lesson 2 of module 2 covers.

Here is what the framework gives you for free, set against what chronicler's requestStructuredJson does by hand:

Map it across: requestStructuredJson's safeParse maps to withStructuredOutput's built-in parse. The four-attempt backoff maps, roughly, to LangChain's model-level maxRetries / retry config. The fence-stripping is mostly unnecessary once you use tool-calling structured output, because the model returns structured content, not a markdown code block. The preservation-bias default is business logic that stays yours either way, no framework replaces a product decision.

Practice

Try it yourself

Quiz

Why fence-stripping mostly disappears

Chronicler's markdown-fence stripping becomes mostly redundant once you use LangChain structured output. Why?

Recall

Where the preservation bias lives

Chronicler defaults omitted messageIds to category B. Is that expressible in a LangChain structured-output schema, or does it live elsewhere?

Where does the omitted-id-defaults-to-B rule live, and why can't the schema itself express it?

Do

Re-implement a day of chronicler classification

Re-implement one day-chunk of chronicler's A/B/C classification using LangChain structured output, then diff it against the cached, hand-rolled result for that same day.

Tick every step to confirm you did it.

Check

Classification parity

Confirm the spike's output against the cached day.

You should see

Same categories as the cached day for more than 95% of messages, with every difference explained.