# Agent run timeline with LangGraph

> Show a LangGraph run step by step: map useStream's discovered nodes to a run timeline, live timers included, and hold the approval card in the waiting step.

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

A LangGraph run is a series of named nodes, and `useStream` from `@langchain/react` discovers each one as it runs in `stream.subgraphs`, with a status and a start time. Map each node to a timeline step: running is active, complete is done, error is failed. When `stream.interrupt` is set, add a waiting step and put the approval card inside it. The timeline then reads like the graph, at the speed it runs.

## How LangGraph reports progress

LangGraph's [Graph execution](https://docs.langchain.com/oss/javascript/langgraph/frontend/graph-execution) page shows the pattern: every graph is built from named nodes, and the frontend hook exposes a snapshot per node it has seen, so the UI needs no hardcoded list of steps. In the `@langchain/langgraph-sdk` 1.12.8 types we checked, a snapshot carries `nodeName`, `status` (`running`, `complete` or `error`), `startedAt` and `completedAt`. The docs page also names a `pending` status; treat any status you do not know as pending.

The page renders nodes as a row of colored pills. A timeline does the same job with more room: a past-tense line per finished step, a live timer on the running one, and a place for the step that waits on a person.

## From node snapshot to step

| LangGraph | Run timeline step |
| --- | --- |
| `node.nodeName` | `id`, and the key into your titles |
| `status: "running"` | `status="active"`, `startedAt` from `node.startedAt` |
| `status: "complete"` | `status="done"`, `endedAt` from `node.completedAt` |
| `status: "error"` | `status="error"`, with what failed in `detail` |
| `stream.interrupt` set | An extra step with `status="waiting"` and an approval card as `children` |
| Not reached yet | Leave it out, or list it as `pending` if you know the plan |

## The code

The graph is any compiled `StateGraph`; this one plans, reads, drafts and then waits for a person before it publishes.

```ts
// agent/release-notes.ts
import { Annotation, END, MemorySaver, START, StateGraph, interrupt } from "@langchain/langgraph";

const State = Annotation.Root({
  prs: Annotation<number>(),
  draft: Annotation<string>(),
  approved: Annotation<boolean>(),
});

export const graph = new StateGraph(State)
  .addNode("collect", async () => ({ prs: 46 }))
  .addNode("draft", async (state) => ({ draft: `Release notes for ${state.prs} merged pull requests` }))
  .addNode("review", (state) => ({ approved: interrupt<{ draft: string }, boolean>({ draft: state.draft }) }))
  .addNode("publish", async () => ({}))
  .addEdge(START, "collect")
  .addEdge("collect", "draft")
  .addEdge("draft", "review")
  .addConditionalEdges("review", (state) => (state.approved ? "publish" : END), ["publish", END])
  .addEdge("publish", END)
  .compile({ checkpointer: new MemorySaver() });
```

The client turns snapshots into steps and keeps one source of truth: the stream.

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

import { useStream } from "@langchain/react";
import { ApprovalCard } from "@/components/pixilui/approval-card";
import { RunTimeline, type RunTimelineStep } from "@/components/pixilui/run-timeline";
import type { graph } from "../agent/release-notes";

// Present tense while it runs, past tense with the evidence once it is done.
const copy: Record<string, { active: string; done: (prs: number) => string }> = {
  collect: { active: "Reading merged pull requests", done: (prs) => `Read ${prs} merged pull requests` },
  draft: { active: "Drafting the notes", done: () => "Drafted the notes" },
  review: { active: "Waiting for review", done: () => "Reviewed" },
  publish: { active: "Publishing to the changelog", done: () => "Published to the changelog" },
};

const statusOf = (status: string): RunTimelineStep["status"] =>
  status === "running" ? "active" : status === "complete" ? "done" : status === "error" ? "error" : "pending";

export function ReleaseRun() {
  const stream = useStream<typeof graph, { draft: string }>({ apiUrl: "http://localhost:2024", assistantId: "release-notes" });
  const pending = stream.interrupt;

  const steps: RunTimelineStep[] = [...stream.subgraphs.values()]
    .filter((node) => node.nodeName !== "review")
    .map((node) => {
      const status = statusOf(node.status);
      const text = copy[node.nodeName];
      return {
        id: node.id,
        title: status === "done" ? (text?.done(stream.values.prs ?? 0) ?? node.nodeName) : (text?.active ?? node.nodeName),
        detail: status === "error" ? "This step failed. Run it again from the last checkpoint." : undefined,
        status,
        startedAt: node.startedAt.getTime(),
        endedAt: node.completedAt?.getTime(),
      };
    });

  if (pending) {
    steps.push({
      id: "review",
      title: "Waiting for your review",
      status: "waiting",
      children: (
        <ApprovalCard
          title="Publish the release notes"
          items={[{ label: "Pull requests", value: String(stream.values.prs ?? 0), numeric: true }]}
          source="Release agent"
          labels={{ approve: "Approve and publish" }}
          onApprove={() => stream.respond(true)}
          onReject={() => stream.respond(false)}
        />
      ),
    });
  }

  return <RunTimeline steps={steps} aria-label="Release notes run" />;
}
```

Install both with `npx shadcn@latest add @pixilui/run-timeline @pixilui/approval-card`.

## What the timeline adds to the snapshots

- **Live timers.** Pass `startedAt` and the running step counts up; pass `endedAt` and it settles to a duration, so a slow node explains a slow run.
- **A waiting state.** `waiting` reads "Needs approval" and holds the card under the step, so the person approves in context instead of in a modal.
- **Evidence lines.** `detail` sits under the title for the counts and links a step produced.
- **Keyboard.** Steps are a list; with `onStepSelect` and `selectedId` a step opens its details in a side pane.

## When the framework already ships a UI

LangGraph's docs list no timeline component. They build the progress bar and node cards from plain markup in the docs page, and Deep Agents has its own frontend patterns (a todo list, subagent cards) for runs that plan their own steps. LangSmith Studio shows the graph and each node's state for developers. The run timeline is for the people using your product, who need to know what the agent did, not the graph's shape.

## When not to use this

- **One node.** A graph that only calls a model once needs a thinking line, not a timeline.
- **Hundreds of steps.** Long loops read better grouped by phase, with the tool calls folded under each.
- **Debugging a graph.** For node inputs, outputs and token counts, use LangSmith traces; the timeline is for users, not for you.

## Questions

### How do I show LangGraph node progress in React?

Use `useStream` from `@langchain/react` and read `stream.subgraphs`, which holds a snapshot per node with its name, status and start time. Map each snapshot to a timeline step; no hardcoded node list is needed.

### How do I show an interrupt in the run timeline?

When `stream.interrupt` is set, add a step with status `waiting` and render the approval card as that step's children. Resume with `stream.respond(value)` and the step moves on as the graph continues.

### Can the timeline survive a page reload mid-run?

LangGraph keeps the run on the server, tied to a thread; the frontend docs describe joining and rejoining a running stream. When the hook reconnects, the snapshots come back and the timeline rebuilds from them.

## Related

- [Showing a multi-step agent run as a timeline](/guides/multi-step-agent-run-timeline): what a run timeline needs, framework aside.
- [Human approval with LangGraph](/guides/human-approval-langgraph): the interrupt in the waiting step, in detail.
- [Agent run timeline with the Vercel AI SDK](/guides/agent-run-timeline-vercel-ai-sdk): the same timeline from data parts.
- [Tool call UI with LangGraph](/guides/tool-call-ui-langgraph): the rows that go under a step.

## Sources

- [LangGraph frontend: Graph execution](https://docs.langchain.com/oss/javascript/langgraph/frontend/graph-execution), read 4 November 2026
- [LangGraph: Interrupts](https://docs.langchain.com/oss/javascript/langgraph/interrupts), read 4 November 2026
- [LangChain frontend: Join and rejoin streams](https://docs.langchain.com/oss/javascript/langchain/frontend/join-rejoin), read 4 November 2026
- [Deep Agents frontend: Overview](https://docs.langchain.com/oss/javascript/deepagents/frontend/overview), read 4 November 2026
- [LangSmith Studio](https://docs.langchain.com/langsmith/studio), read 4 November 2026
- [@langchain/langgraph-sdk on npm](https://www.npmjs.com/package/@langchain/langgraph-sdk) (version 1.12.8, its `SubgraphDiscoverySnapshot` type), read 4 November 2026
