# Agent run timeline with the Vercel AI SDK

> Stream an agent's steps as AI SDK data parts, update each by its id, and render them as a run timeline with live timers next to the answer useChat streams.

Published 2026-11-04. Facts checked 2026-11-04. Source: https://pixilui.com/guides/agent-run-timeline-vercel-ai-sdk

In the AI SDK, send each step of a run as a custom data part, `data-step`, from `createUIMessageStream`. Write the same part again with the same `id` when the step moves on, and the client replaces it in place; that is the SDK's data part reconciliation. On the client, collect the message's `data-step` parts and pass them to a run timeline: one step each, with status, title, detail and times.

## How the AI SDK streams progress

The [Streaming Custom Data](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data) page describes three things a route can send next to the model's text: data parts, which land in `message.parts` as `data-<name>`; sources; and transient parts, which only reach the `onData` callback. A data part written with an `id` is updated, not appended, when the same `id` is written again. The page's example is a weather card going from loading to success; a run step going from active to done is the same move.

You type the parts once, as the second type argument of `UIMessage`, and both `writer.write` on the server and `message.parts` on the client know the shape.

Steps written as data parts are part of the message history, so a reloaded thread shows the finished run. Use a transient part for anything you do not want to keep, like a "connecting" line.

## From data part to step

| What the route writes | Run timeline step |
| --- | --- |
| `{ type: "data-step", id, data: { status: "active", startedAt } }` | Active, live timer from `startedAt` |
| The same `id` with `status: "done"`, `endedAt`, `detail` | Done, duration, evidence line |
| The same `id` with `status: "error"`, `detail` | Failed, with what went wrong |
| A step written as `pending` up front | Listed as up next, so the person sees the plan |
| A tool part in `approval-requested` | A `waiting` step holding an [approval card](/guides/human-approval-vercel-ai-sdk) |

## The code

One type for the message, shared by the route and the component.

```ts
// lib/run-message.ts
import type { UIMessage } from "ai";

export type StepData = {
  title: string;
  status: "pending" | "active" | "done" | "error";
  detail?: string;
  startedAt?: number;
  endedAt?: number;
};

export type RunMessage = UIMessage<never, { step: StepData }>;
```

The route announces the plan, then updates each step by its `id` as the work happens, and streams the model's summary last.

```ts
// app/api/dunning/route.ts
import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, streamText, toUIMessageStream } from "ai";
import type { RunMessage, StepData } from "../../../lib/run-message";

declare function listFailedPayments(): Promise<{ id: string; customer: string; amountCents: number }[]>;
declare function draftReminders(count: number): Promise<number>;

export async function POST(req: Request) {
  const { messages }: { messages: RunMessage[] } = await req.json();

  const stream = createUIMessageStream<RunMessage>({
    execute: async ({ writer }) => {
      const step = (id: string, data: StepData) => writer.write({ type: "data-step", id, data });
      writer.write({ type: "start" });
      step("read", { title: "Reading failed payments", status: "active", startedAt: Date.now() });
      step("draft", { title: "Draft reminders", status: "pending" });

      const failed = await listFailedPayments();
      step("read", { title: `Read ${failed.length} failed payments`, status: "done", detail: "From Stripe, last 7 days", endedAt: Date.now() });

      step("draft", { title: "Drafting reminders", status: "active", startedAt: Date.now() });
      const drafted = await draftReminders(failed.length);
      step("draft", { title: `Drafted ${drafted} reminders`, status: "done", endedAt: Date.now() });

      const result = streamText({ model: "openai/gpt-5.5", system: "Summarize the run in two sentences.", messages: await convertToModelMessages(messages) });
      writer.merge(toUIMessageStream({ stream: result.stream, sendStart: false }));
    },
  });

  return createUIMessageStreamResponse({ stream });
}
```

The client reads the step parts of the last assistant message.

```tsx
// components/dunning-run.tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { RunTimeline, type RunTimelineStep } from "@/components/pixilui/run-timeline";
import type { RunMessage } from "../lib/run-message";

export function DunningRun() {
  const { messages, sendMessage, status } = useChat<RunMessage>();
  const run = messages.findLast((message) => message.role === "assistant");

  const steps: RunTimelineStep[] = (run?.parts ?? []).flatMap((part) =>
    part.type === "data-step" && part.id ? [{ id: part.id, ...part.data }] : [],
  );

  return (
    <section aria-label="Dunning run">
      <button type="button" disabled={status === "streaming"} onClick={() => sendMessage({ text: "Chase this week's failed payments" })}>
        Chase failed payments
      </button>
      {steps.length ? <RunTimeline steps={steps} /> : null}
    </section>
  );
}
```

Install the timeline with `npx shadcn@latest add @pixilui/run-timeline`.

## What the timeline adds to the parts

- **Timers that count.** `startedAt` on an active step runs a live timer; `endedAt` settles it into a duration.
- **Glyphs that change in place.** A step's mark swaps from up next to active to a drawn check, and the connector below fills as it finishes.
- **Evidence under each step.** `detail` holds the count or source a step produced, in past tense.
- **A waiting step.** Put an approval card in a step's `children` and it stays in place after the decision, as a receipt.

## When the framework already ships a UI

The AI SDK has no timeline component. AI Elements, Vercel's component set for the AI SDK, has `Task`, `Plan` and `Chain of Thought`; its site says it is moving into shadcn/ui and will be deprecated at the end of November 2026. The [AI Elements migration doc](/docs/migrate-from-ai-elements) maps those to the run timeline. For durable runs that outlive a request, the AI SDK's `WorkflowAgent` runs the same agent loop inside a durable workflow; the steps you show stay the same.

## When not to use this

- **The model decides every step.** If you only run a tool loop, the tool parts are the steps; render them as [tool call rows](/guides/tool-call-ui-vercel-ai-sdk) instead of inventing steps.
- **Sub-second runs.** Three steps that finish in 400 ms flash by; a thinking line is enough.
- **Progress you cannot measure.** A step with no real start and end is decoration. Only write steps your code can mark done.

## Questions

### How do I show agent progress with the Vercel AI SDK?

Write each step as a data part (`data-step`) with an `id` from `createUIMessageStream`, and write it again with the same `id` as it changes. On the client, the parts are in `message.parts`; map them to timeline steps.

### What is data part reconciliation in the AI SDK?

When the server writes a data part with an `id` that the message already has, the client replaces that part instead of adding a new one. The AI SDK docs use it for loading states that become results.

### Do data parts stay in the chat history?

Yes, unless you mark them `transient: true`. Transient parts only reach the `onData` callback and are not added to the message.

## Related

- [Showing a multi-step agent run as a timeline](/guides/multi-step-agent-run-timeline): what a run timeline needs, framework aside.
- [Agent run timeline with LangGraph](/guides/agent-run-timeline-langgraph): the same timeline from graph nodes.
- [Agent run timeline with the OpenAI Agents SDK](/guides/agent-run-timeline-openai-agents-sdk): the same timeline from run item events.
- [Tool call UI with the Vercel AI SDK](/guides/tool-call-ui-vercel-ai-sdk): rows for the calls inside each step.

## Sources

- [AI SDK UI: Streaming Custom Data](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data), read 4 November 2026
- [AI SDK: WorkflowAgent](https://ai-sdk.dev/docs/agents/workflow-agent), read 4 November 2026
- [AI Elements](https://elements.ai-sdk.dev), read 4 November 2026
