惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
www.infosecurity-magazine.com
www.infosecurity-magazine.com
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - Franky
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Cloudflare Blog
N
News and Events Feed by Topic
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
AWS News Blog
AWS News Blog
S
SegmentFault 最新的问题
T
Tailwind CSS Blog
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Spread Privacy
Spread Privacy
J
Java Code Geeks
博客园 - 聂微东
T
Tor Project blog
宝玉的分享
宝玉的分享
博客园 - 叶小钗
Webroot Blog
Webroot Blog
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
H
Heimdal Security Blog
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
I
InfoQ
Security Latest
Security Latest
Martin Fowler
Martin Fowler
Hacker News: Ask HN
Hacker News: Ask HN
P
Privacy International News Feed
C
CERT Recently Published Vulnerability Notes
Latest news
Latest news
雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
H
Help Net Security
L
LINUX DO - 最新话题
L
LINUX DO - 热门话题

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow serialization-failed start-invalid-workflow-function step-not-registered timeout-in-workflow webhook-invalid-respond-with-value webhook-response-not-sent workflow-not-registered Errors & Retrying Hooks & Webhooks Idempotency Foundations Serialization Starting Workflows Streaming Versioning Workflows and Steps How the Directives Work Encryption Event Sourcing Framework Integrations Understanding Directives Migration Guides Migrating from AWS Step Functions Migrating from Inngest Migrating from Temporal Observability Testing Server-Based Testing createHook createWebhook defineHook FatalError fetch getStepMetadata getWorkflowMetadata getWritable workflow RetryableError sleep @workflow/vitest DurableAgent @workflow/ai WorkflowChatTransport getHookByToken getRun getWorld workflow/api resumeHook resumeWebhook Chat Session Modeling runtime-decryption-failed Upgrading Workflows abort-signal-timeout-in-workflow Cancellation How Cancellation Works Internal Serializable AbortController and AbortSignal Eager Processing of Steps & Incremental Event Replay TanStack Start Agent Cancellation Sequential & Parallel Execution Workflow Composition Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Resumable Streams
2026-05-31 · via Workflow SDK Documentation

Handle network interruptions, page refreshes, and timeouts without losing agent progress.

When building chat interfaces, it's common to run into network interruptions, page refreshes, or serverless function timeouts, which can break the connection to an in-progress agent.

Where a standard chat implementation would require the user to resend their message and wait for the entire response again, workflow runs are durable, and so are the streams attached to them. This means a stream can be resumed at any point, optionally only syncing the data that was missed since the last connection.

Resumable streams come out of the box with Workflow SDK, however, the client needs to recognize that a stream exists, and needs to know which stream to reconnect to, and needs to know where to start from. For this, Workflow SDK provides the WorkflowChatTransport helper, a drop-in transport for the AI SDK that handles client-side resumption logic for you.

Let's add stream resumption to our Flight Booking Agent that we build in the Building Durable AI Agents guide.

Return the Run ID from Your API

Modify your chat endpoint to include the workflow run ID in a response header. The Run ID uniquely identifies the run's stream, so it allows the client to know which stream to reconnect to.

// ... imports ...

export async function POST(req: Request) {

  // ... existing logic to create the workflow ...

  const run = await start(chatWorkflow, [modelMessages]);

  return createUIMessageStreamResponse({
    stream: run.readable,
    headers: { 
      "x-workflow-run-id": run.runId, 
    }, 
  });
}

Add a Stream Reconnection Endpoint

Currently we only have one API endpoint that always creates a new run, so we need to create a new API route that returns the stream for an existing run:

import { createUIMessageStreamResponse } from "ai";
import { getRun } from "workflow/api"; 

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const { searchParams } = new URL(request.url);

  // Client provides the last chunk index they received
  const startIndexParam = searchParams.get("startIndex"); 
  const startIndex = startIndexParam
    ? parseInt(startIndexParam, 10)
    : undefined;

  // Instead of starting a new run, we fetch an existing run.
  const run = getRun(id); 
  const readable = run.getReadable({ startIndex }); 

  // Provide the stream's tail index so the transport can resolve
  // negative startIndex values into absolute positions for retries.
  const tailIndex = await readable.getTailIndex(); 

  return createUIMessageStreamResponse({
    stream: readable, 
    headers: { 
      "x-workflow-stream-tail-index": String(tailIndex), 
    }, 
  });
}

The startIndex parameter ensures the client can choose where to resume the stream from. For instance, if the function times out during streaming, the chat transport will use startIndex to resume the stream exactly from the last token it received. Negative values are also supported (e.g. -5 starts 5 chunks before the end), which is useful for custom stream consumers (such as a dashboard showing recent output) that want to show the most recent output without replaying the full stream.

When using a negative startIndex, your stream endpoint must return a x-workflow-stream-tail-index header in order for relative resumption to work. Missing the header will fall back to replaying the entire stream.

Use WorkflowChatTransport in the Client

Replace the default transport in AI-SDK's useChat with WorkflowChatTransport, and update the callbacks to store and use the latest run ID. For now, we'll store the run ID in localStorage. For your own app, this would be stored wherever you store session information.

"use client";

import { useChat } from "@ai-sdk/react";
import { WorkflowChatTransport } from "@workflow/ai"; 
import { useMemo, useState } from "react";

export default function ChatPage() {

  // Check for an active workflow run on mount
  const activeRunId = useMemo(() => { 
    if (typeof window === "undefined") return; 
    return localStorage.getItem("active-workflow-run-id") ?? undefined; 
  }, []); 

  const { messages, sendMessage, status } = useChat({
    resume: Boolean(activeRunId), 
    transport: new WorkflowChatTransport({ 
      api: "/api/chat",

      // Store the run ID when a new chat starts
      onChatSendMessage: (response) => { 
        const workflowRunId = response.headers.get("x-workflow-run-id"); 
        if (workflowRunId) { 
          localStorage.setItem("active-workflow-run-id", workflowRunId); 
        } 
      }, 

      // Clear the run ID when the chat completes
      onChatEnd: () => { 
        localStorage.removeItem("active-workflow-run-id"); 
      }, 

      // Use the stored run ID for reconnection
      prepareReconnectToStreamRequest: ({ api, ...rest }) => { 
        const runId = localStorage.getItem("active-workflow-run-id"); 
        if (!runId) throw new Error("No active workflow run ID found"); 
        return { 
          ...rest, 
          api: `/api/chat/${encodeURIComponent(runId)}/stream`, 
        }; 
      }, 
    }), 
  });

  // ... render your chat UI
}

Now try the flight booking example again. Open it up in a separate tab, or spam the refresh button, and see how the client connects to the same chat stream every time.

  1. When the user sends a message, WorkflowChatTransport makes a POST to /api/chat
  2. The API starts a workflow and returns the run ID in the x-workflow-run-id header
  3. onChatSendMessage stores this run ID in localStorage
  4. If the stream is interrupted before receiving a "finish" chunk, the transport automatically reconnects
  5. prepareReconnectToStreamRequest builds the reconnection URL using the stored run ID, pointing to the new endpoint /api/chat/{runId}/stream
  6. The reconnection endpoint returns the stream from where the client left off
  7. When the stream completes, onChatEnd clears the stored run ID

This approach also handles page refreshes, as the client will automatically reconnect to the stream from the last known position when the UI loads with a stored run ID, following the behavior of AI SDK's stream resumption.

Resuming from the end of the stream

By default, reconnecting replays the entire stream from the beginning (startIndex: 0). If you only need to show recent output — for example, when resuming a long conversation after a page refresh — you can set initialStartIndex to a negative value to read from the end of the stream instead:

const { messages, sendMessage } = useChat({
  resume: !!activeWorkflowRunId,
  transport: new WorkflowChatTransport({
    initialStartIndex: -20, // Only fetch the last 20 chunks
    // ... callbacks as above
  }),
});

This avoids replaying potentially thousands of chunks and lets the UI render faster. The negative value is resolved server-side, so -20 on a 500-chunk stream starts at chunk 480.

When using a negative initialStartIndex, the reconnection endpoint must return the x-workflow-stream-tail-index header (as shown in Step 2 above). The transport uses this header to compute absolute chunk positions so that retries after a disconnect resume from the correct position. If the header is missing, the transport falls back to startIndex: 0 (replaying the entire stream) and logs a warning.