# Tool call UI with LangGraph

> Render the tool calls of a LangGraph or LangChain agent as tool call rows: read stream.toolCalls from useStream, map running, finished and error, and fold the JSON.

Published 2026-11-04. Facts checked 2026-11-04. Source: https://pixilui.com/guides/tool-call-ui-langgraph

With LangGraph and LangChain agents, `useStream` from `@langchain/react` gathers every tool call into one array, `stream.toolCalls`. Each entry has the tool's name, its input, its output once it returns, and a status of `running`, `finished` or `error`. Render each as a tool call row, match calls to the AI message that made them by `callId`, and group one message's calls under a line that counts them.

## How LangGraph exposes tool calls

The frontend [Tool calling](https://docs.langchain.com/oss/javascript/langchain/frontend/tool-calling) page explains the merge: the agent emits tool calls inside an AI message, the runtime runs each tool, the result comes back as a `ToolMessage`, and `useStream` unifies call and result into one `AssembledToolCall`. The docs list its fields: `name`, `callId` (also `id`), `namespace`, `input` (also `args`), `output` (null until the call succeeds), `status` and `error`.

To put calls under the right message, the docs filter `stream.toolCalls` by the IDs in that message's `tool_calls`. The docs then render each call as a loading card, an error card or a tool-specific card. A row is the quieter default: one line per call, with the tool-specific card below it when a result deserves one.

## From AssembledToolCall to row

| `AssembledToolCall` | Tool call row |
| --- | --- |
| `name` | `name`, shown in mono |
| `status: "running"` | `status="running"`, live timer, present-tense summary |
| `status: "finished"` | `status="done"`, duration, past-tense summary with the count |
| `status: "error"` | `status="error"`, `error` in place, Retry |
| `input` | `input`, folded JSON with Copy |
| `output` | `output`, folded JSON with Copy |
| `namespace` | Which subgraph ran it; group by it if you show subagents |

## The code

A LangChain agent with one tool. `useStream` reads it from a LangGraph server.

```ts
// agent/support.ts
import { MemorySaver } from "@langchain/langgraph";
import { createAgent, tool } from "langchain";
import { z } from "zod";

const searchOrders = tool(async ({ email }) => JSON.stringify({ email, orders: 3, lastOrder: "NW-20418" }), {
  name: "search_orders",
  description: "Find a customer's orders by email",
  schema: z.object({ email: z.string() }),
});

export const agent = createAgent({ model: "openai:gpt-5.5", tools: [searchOrders], checkpointer: new MemorySaver() });
```

The client renders the calls of one AI message as rows.

```tsx
// components/message-tool-calls.tsx
"use client";

import { useRef } from "react";
import type { AssembledToolCall } from "@langchain/react";
import { ToolCallRow, ToolCallRowGroup, type ToolCallRowStatus, type ToolCallRowValue } from "@/components/pixilui/tool-call-row";

const json = (value: unknown) => (value == null ? undefined : (JSON.parse(JSON.stringify(value)) as ToolCallRowValue));

const statusOf = (call: AssembledToolCall): ToolCallRowStatus => (call.status === "finished" ? "done" : call.status);

// Present tense while it runs, past tense with the evidence once it is done.
function summary(call: AssembledToolCall) {
  if (call.name === "search_orders") {
    const found = call.status === "finished" ? (JSON.parse(String(call.output)) as { orders: number }).orders : undefined;
    return found === undefined ? "Searching orders" : `Found ${found} orders`;
  }
  return call.status === "finished" ? `Ran ${call.name}` : `Running ${call.name}`;
}

export function MessageToolCalls({ callIds, toolCalls }: { callIds: string[]; toolCalls: AssembledToolCall[] }) {
  // The calls carry no timing: note when each one first shows up and when it settles.
  const times = useRef(new Map<string, { start: number; end?: number }>());
  const calls = toolCalls.filter((call) => callIds.includes(call.callId));
  if (!calls.length) return null;

  return (
    <ToolCallRowGroup>
      {calls.map((call) => {
        const time = times.current.get(call.callId) ?? { start: Date.now() };
        if (call.status !== "running" && time.end === undefined) time.end = Date.now();
        times.current.set(call.callId, time);
        return (
          <ToolCallRow
            key={call.callId}
            name={call.name}
            status={statusOf(call)}
            summary={summary(call)}
            startedAt={time.start}
            durationMs={time.end !== undefined ? time.end - time.start : undefined}
            input={json(call.input)}
            output={json(call.output)}
            error={call.error}
          />
        );
      })}
    </ToolCallRowGroup>
  );
}
```

Use it under each AI message: `<MessageToolCalls callIds={message.tool_calls?.map((t) => t.id ?? "") ?? []} toolCalls={stream.toolCalls} />`, where `stream` is `useStream<typeof agent>(...)`. Install the row with `npx shadcn@latest add @pixilui/tool-call-row`.

## What the row adds to the tool call

- **One quiet line per call.** The docs' loading and error cards become states of one row, so a message with four calls stays four lines.
- **A summary in words.** "Found 3 orders" for the person; `search_orders` in mono for the developer.
- **Time.** A live timer while `running`, the duration once `finished`.
- **The raw payload, folded.** Input and output as formatted JSON with Copy, closed by default.

## When the framework already ships a UI

LangGraph's docs list no tool call component; they build the cards in plain markup and name AI Elements, assistant-ui, CopilotKit and OpenUI as libraries that can render on top of `useStream`. If one of those already renders your chat, use its tool UI. LangSmith shows every tool call in a trace for developers; the row is for the people using your product.

## When not to use this

- **A tool whose result is the answer.** A weather card or chart should render as itself; keep the row only if the call needs a record.
- **Hidden plumbing.** Retrieval and routing calls can be noise; show the calls that changed the answer.
- **Headless tools on the client.** Tools that run in the browser (the docs call them headless tools) may need their own UI, such as a permission prompt.

## Questions

### How do I show tool calls from a LangGraph agent in React?

Use `useStream` from `@langchain/react` and read `stream.toolCalls`. Each entry joins the call and its result, with `name`, `input`, `output`, `status` and `error`. Map the status to a row's running, done or error state.

### How do I show only one message's tool calls?

Filter `stream.toolCalls` by the IDs in that AI message's `tool_calls`. The LangChain frontend docs match on `callId`.

### Do LangGraph tool calls include timing?

The `AssembledToolCall` fields the docs list carry no timestamps. Note when a call first appears and when its status leaves `running`, or return the duration from the tool itself.

## Related

- [How to show tool calls and their results in an AI chat](/guides/show-tool-calls-in-ai-chat): what a tool row needs, framework aside.
- [Agent run timeline with LangGraph](/guides/agent-run-timeline-langgraph): the steps the calls belong to.
- [Tool call UI with the Vercel AI SDK](/guides/tool-call-ui-vercel-ai-sdk): the same row from AI SDK tool parts.
- [Tool call UI with the OpenAI Agents SDK](/guides/tool-call-ui-openai-agents-sdk): the same row from run item events.

## Sources

- [LangChain frontend: Tool calling](https://docs.langchain.com/oss/javascript/langchain/frontend/tool-calling), read 4 November 2026
- [LangChain frontend: Overview](https://docs.langchain.com/oss/javascript/langchain/frontend/overview), read 4 November 2026
- [@langchain/langgraph-sdk on npm](https://www.npmjs.com/package/@langchain/langgraph-sdk) (version 1.12.8, its `AssembledToolCall` type), read 4 November 2026
