Introducing Experimental Workflows and Orchestrators in TanStack AI TanStack released experimental AI Workflows and Orchestrators on May 28, 2026, enabling developers to compose multi-step LLM and agent processes as TypeScript async generators with streaming, human-in-the-loop approval, and SSE-based state updates. The pre-release packages, available via pkg.pr.new for testing and feedback, aim to replace hand-rolled workflow engines with typed, composable orchestration mechanisms that pause for human input and resume through a single streaming flow. by Alem Tuzlak on May 28, 2026. Most AI apps start with one chat call. But as soon as you need something more complex, this all breaks apart. You either fall back to using sub-agents as tools, or you have to write your own glue and abstractions on top to make a semi-decent workflow or orchestration mechanism to power your app. This just detracts from your time to work on the features you really care about. The model needs to draft, critique, revise, ask for approval, call another model, update state, and show the user what is happening while the run is still in progress. At that point, a one-shot chat endpoint turns into a hand-rolled workflow engine made of fetch calls, temporary state, custom SSE events, and a lot of code nobody wanted to own. Today, TanStack introduces an experimental answer: TanStack AI Workflows & Orchestrators . Before we go further, a fair warning This is not merged to main. It is not shipped, stable, or available in normal npm versions. It is a PR build you can try today through pkg.pr.new while the API is still being shaped. The goal is to get it in front of real use cases, demos, and feedback before we commit to the public API shape. This is where you come in. We need your help. We need people to test out our workflows and our orchestration mechanisms, give us their thoughts and opinions, and help us shape the final APIs. The goal is simple: compose multiple typed LLM and agent steps as normal TypeScript async generators, stream each step to the UI, pause for human approval, and resume through the same SSE flow. Install the PR packages directly from pkg.pr.new: npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-orchestration@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-orchestration@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@542 npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@542 Use this only for evaluation, demos, and feedback. The public API can still change before stabilization. Full documentation for this PR lives on the GitHub branch, not the released TanStack docs site: Agents are typed wrappers around a chat call or any async function. defineAgent gives each step an input schema, output schema, and implementation. js import { chat } from '@tanstack/ai' import { defineAgent } from '@tanstack/ai-orchestration' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' export const ArticleSchema = z.object { title: z.string , body: z.string , } export const writer = defineAgent { name: 'writer', input: z.object { topic: z.string , } , output: ArticleSchema, run: { input } = chat { adapter: openaiText 'gpt-4o' , outputSchema: ArticleSchema, stream: true, systemPrompts: 'Write a concise developer article with a clear title and practical body.', , messages: { role: 'user', content: input.topic } , } , } export const editor = defineAgent { name: 'editor', input: z.object { article: ArticleSchema, feedback: z.string .optional , } , output: z.object { approved: z.boolean , article: ArticleSchema, notes: z.string , } , run: { input } = chat { adapter: openaiText 'gpt-4o' , outputSchema: z.object { approved: z.boolean , article: ArticleSchema, notes: z.string , } , stream: true, systemPrompts: 'Edit the article for accuracy, clarity, and practical developer tone. Apply any optional reviewer feedback.', , messages: { role: 'user', content: JSON.stringify input } , } , } js import { chat } from '@tanstack/ai' import { defineAgent } from '@tanstack/ai-orchestration' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' export const ArticleSchema = z.object { title: z.string , body: z.string , } export const writer = defineAgent { name: 'writer', input: z.object { topic: z.string , } , output: ArticleSchema, run: { input } = chat { adapter: openaiText 'gpt-4o' , outputSchema: ArticleSchema, stream: true, systemPrompts: 'Write a concise developer article with a clear title and practical body.', , messages: { role: 'user', content: input.topic } , } , } export const editor = defineAgent { name: 'editor', input: z.object { article: ArticleSchema, feedback: z.string .optional , } , output: z.object { approved: z.boolean , article: ArticleSchema, notes: z.string , } , run: { input } = chat { adapter: openaiText 'gpt-4o' , outputSchema: z.object { approved: z.boolean , article: ArticleSchema, notes: z.string , } , stream: true, systemPrompts: 'Edit the article for accuracy, clarity, and practical developer tone. Apply any optional reviewer feedback.', , messages: { role: 'user', content: JSON.stringify input } , } , } defineAgent wraps either a chat call or a normal async function with input and output schemas. The workflow runtime uses those schemas to validate what enters and leaves the step, and TypeScript uses them to infer the callable shape inside a workflow. From the workflow's perspective, writer and editor are normal typed async steps. Once agents exist, defineWorkflow composes them with yield inside an async function . js import { approve, defineWorkflow, fail, succeed, } from '@tanstack/ai-orchestration' import { z } from 'zod' import { ArticleSchema, editor, writer } from './agents' const ArticleInputSchema = z.object { topic: z.string , } const ArticleWorkflowOutputSchema = z.discriminatedUnion 'ok', z.object { ok: z.literal true , article: ArticleSchema, } , z.object { ok: z.literal false , reason: z.string , } , export const articleWorkflow = defineWorkflow { name: 'article-workflow', input: ArticleInputSchema, output: ArticleWorkflowOutputSchema, agents: { writer, editor, }, run: async function { input, agents } { const draft = yield agents.writer { topic: input.topic } const edited = yield agents.editor { article: draft } if edited.approved { return fail edited.notes } const decision = yield approve { title: Publish "${edited.article.title}"? , description: edited.notes, } if decision.approved { return fail decision.feedback ?? 'Publication declined' } return succeed { article: edited.article } }, } js import { approve, defineWorkflow, fail, succeed, } from '@tanstack/ai-orchestration' import { z } from 'zod' import { ArticleSchema, editor, writer } from './agents' const ArticleInputSchema = z.object { topic: z.string , } const ArticleWorkflowOutputSchema = z.discriminatedUnion 'ok', z.object { ok: z.literal true , article: ArticleSchema, } , z.object { ok: z.literal false , reason: z.string , } , export const articleWorkflow = defineWorkflow { name: 'article-workflow', input: ArticleInputSchema, output: ArticleWorkflowOutputSchema, agents: { writer, editor, }, run: async function { input, agents } { const draft = yield agents.writer { topic: input.topic } const edited = yield agents.editor { article: draft } if edited.approved { return fail edited.notes } const decision = yield approve { title: Publish "${edited.article.title}"? , description: edited.notes, } if decision.approved { return fail decision.feedback ?? 'Publication declined' } return succeed { article: edited.article } }, } The interesting part is the lack of framework ceremony. TypeScript knows the input expected by agents.writer, and it knows the shape returned after the yield . The runtime can emit lifecycle events around each yielded step, stream text while it runs, validate the result, snapshot state, and resume the generator with the typed output. Why async generator workflows? There are a lot of ways to model agent workflows. You can build a graph DSL. You can define nodes in JSON. You can describe a DAG and ask the runtime to interpret it. Well, the reason we decided to go with generator workflows is because whenever you yield the agent's step, it's streamed straight down to the client. The user sees everything in real time, and then, by the end of it, you just get the final output back. The user saw everything that went on: tool calls, reasoning, whatever. The workflow body is just TypeScript. Use if, for, while, try, await, helper functions, and whatever domain code you already have. The orchestration runtime only cares about the things you yield . Each yield agents.someAgent ... becomes a typed step. The runtime can emit lifecycle events around it, stream text while it runs, validate the result, snapshot state, and resume the generator with the typed output. Workflows run on the server. The browser consumes an event stream. The PR adds parseWorkflowRequest, runWorkflow, and inMemoryRunStore from @tanstack/ai-orchestration. You can pipe the returned stream through the existing toServerSentEventsResponse helper from @tanstack/ai. js import { toServerSentEventsResponse } from '@tanstack/ai' import { inMemoryRunStore, parseWorkflowRequest, runWorkflow, } from '@tanstack/ai-orchestration' import { articleWorkflow } from './article-workflow' const runStore = inMemoryRunStore { ttl: 60 60 1000 } export async function POST request: Request { const params = await parseWorkflowRequest request const stream = runWorkflow { workflow: articleWorkflow, runStore, ...params, } return toServerSentEventsResponse stream } js import { toServerSentEventsResponse } from '@tanstack/ai' import { inMemoryRunStore, parseWorkflowRequest, runWorkflow, } from '@tanstack/ai-orchestration' import { articleWorkflow } from './article-workflow' const runStore = inMemoryRunStore { ttl: 60 60 1000 } export async function POST request: Request { const params = await parseWorkflowRequest request const stream = runWorkflow { workflow: articleWorkflow, runStore, ...params, } return toServerSentEventsResponse stream } runWorkflow emits AG-UI-style lifecycle events for the run. That includes run and step events, state snapshots, JSON Patch state deltas, output, and errors. The UI does not need to invent its own event protocol for "writer started", "editor streamed text", "approval requested", or "run finished". The current built-in persistence is inMemoryRunStore. That is useful for local demos and single-process evaluation. Production durability is still future-facing and experimental run-store-interface territory, especially for long pauses, deploys, restarts, and multi-node environments. But the API is there to implement your own durable run store and swap it in when you're ready. On the client, WorkflowClient, useWorkflow, and useOrchestration consume the streamed events and keep local run state updated. js import { fetchWorkflowEvents, useWorkflow } from '@tanstack/ai-react' import { z } from 'zod' const ArticleSchema = z.object { title: z.string , body: z.string , } const ArticleInputSchema = z.object { topic: z.string , } const ArticleWorkflowOutputSchema = z.discriminatedUnion 'ok', z.object { ok: z.literal true , article: ArticleSchema, } , z.object { ok: z.literal false , reason: z.string , } , export function ArticleWorkflowDemo { const workflow = useWorkflow { input: ArticleInputSchema, output: ArticleWorkflowOutputSchema, connection: fetchWorkflowEvents '/api/article-workflow' , } return