Structured Output That Remembers Across Turns TanStack's AI SDK now preserves structured output across chat turns by attaching typed data to each message, eliminating the need for manual state management. The update adds a `structured-output` part type to every `UIMessage`, allowing developers to access validated objects from previous turns without custom history plumbing. This change simplifies multi-turn workflows like recipe refinement and form filling, reducing boilerplate code in applications using the `useChat` hook with `outputSchema`. by Alem Tuzlak on May 19, 2026. You ask the LLM for a recipe. It plates up Spaghetti Pomodoro — title, cuisine, servings, ingredients, steps, all typed against your schema. Beautiful. You ask it to make the recipe vegan. A new recipe streams in. Then the first one vanishes. That used to be the deal with useChat { outputSchema } : one hook-level partial / final slot. The instant a new turn started streaming, the previous turn's recipe was clobbered. Multi-turn anything recipe refinement, ticket triage, iterative form filling needed manual history plumbing — you'd intercept every chunk, snapshot final into your own state, juggle a recipes array yourself, and keep it in sync with messages to avoid drift. The schema's type safety also stopped at partial / final; once a structured payload landed in your local array, it was unknown again unless you cast. We just shipped a different shape. Every assistant turn now carries its own typed structured-output part on its UIMessage. History is preserved by default. The schema generic threads all the way down to messages i .parts.find p = p.type === 'structured-output' .data — no casts, no manual tracking. Same useChat hook, same outputSchema option, less code in your component. If you missed the prior post on streaming a single typed object end-to-end, start here https://tanstack.com/blog/streaming-structured-output — that piece is the antecedent to this one. This post walks through what changed, why it matters for any multi-turn UI, and how to build the recipe-builder pattern end-to-end in roughly 80 lines split across a server route and a client component . The previous useChat { outputSchema } exposed two values that tracked the current run: On a single-turn extractor paste a paragraph → get a typed Person , this was perfect. Field-by-field reveal as the JSON streamed, validated payload on terminal event, one render to consume both. On a multi-turn chat it fell apart. partial and final were a single slot , scoped to whichever run was most recent. As soon as you called sendMessage again, the previous turn's final was gone. The runtime had no place to keep it — the typed structured payload didn't live on the message itself, only in this transient hook state. The workaround we'd see in user code: const recipes, setRecipes = useState