State Rendering
Render your agent's state with custom UI components in real-time.
What is this?#
State rendering lets you build UI that reflects your agent's state in real-time. As your agent progresses through nodes and emits state updates, your frontend renders those changes — showing progress, drafts, or intermediate results.
When should I use this?#
Use state rendering when you want to:
- Show real-time progress (e.g. "Researching... 2/5 complete")
- Display drafts that update as the agent works
- Build dashboards that reflect agent state
- Render structured output outside of the chat
Implementation#
Run and connect your agent#
You'll need to run your agent and connect it to CopilotKit before proceeding. If you haven't done so already, you can follow the instructions in the Getting Started guide.
If you don't already have an agent, you can use the coagent starter as a starting point as this guide uses it as a starting point.
Build an agent that produces state#
Define the searches state, then add a tool that returns each completed update.
from typing import Any, TypedDict
from copilotkit import (
CopilotKitMiddleware,
CopilotKitState,
StateItem,
StateStreamingMiddleware,
)
from deepagents import create_deep_agent
from langchain.agents.middleware import AgentMiddleware
from langchain.messages import ToolMessage
from langchain.tools import ToolRuntime, tool
from langgraph.types import Command
class Search(TypedDict):
query: str
done: bool
class AgentState(CopilotKitState):
searches: list[Search]
class SearchesStateMiddleware(AgentMiddleware[AgentState, Any, Any]):
state_schema = AgentState
@tool
def report_research_progress(
searches: list[Search],
runtime: ToolRuntime[None, AgentState],
) -> Command:
"""Report the current research tasks and completion status."""
return Command(
update={
"searches": searches,
"messages": [
ToolMessage(
content="Research progress saved.",
tool_call_id=runtime.tool_call_id,
)
],
}
)
agent = create_deep_agent(
model="openai:gpt-5.4",
tools=[report_research_progress],
middleware=[
SearchesStateMiddleware(),
CopilotKitMiddleware(),
StateStreamingMiddleware(
StateItem(
state_key="searches",
tool="report_research_progress",
tool_argument="searches",
)
),
],
system_prompt=(
"You are a research assistant. Use report_research_progress "
"to show each task and mark it done when complete."
),
)import { ToolMessage } from "@langchain/core/messages";
import { tool, type ToolRuntime } from "@langchain/core/tools";
import { Command } from "@langchain/langgraph";
import {
copilotkitMiddleware,
zodState,
} from "@copilotkit/sdk-js/langgraph";
import {
stateItem,
stateStreamingMiddleware,
} from "@copilotkit/sdk-js/langgraph-middlewares";
import { createDeepAgent } from "deepagents";
import { createMiddleware } from "langchain";
import { z } from "zod";
const SearchSchema = z.object({
query: z.string(),
done: z.boolean(),
});
type Search = z.infer<typeof SearchSchema>;
const SearchesStateSchema = z.object({
searches: z.array(SearchSchema),
});
const searchesStateMiddleware = createMiddleware({
name: "SearchesState",
stateSchema: z.object({
searches: zodState(z.array(SearchSchema).default(() => [])),
}),
});
const reportResearchProgress = tool(
(
input: { searches: Search[] },
runtime: ToolRuntime<typeof SearchesStateSchema>,
) =>
new Command({
update: {
searches: input.searches,
messages: [
new ToolMessage({
content: "Research progress saved.",
tool_call_id: runtime.toolCallId,
}),
],
},
}),
{
name: "report_research_progress",
description:
"Report the current research tasks and completion status.",
schema: z.object({ searches: z.array(SearchSchema) }),
},
);
export const agent = createDeepAgent({
model: "openai:gpt-5.4",
tools: [reportResearchProgress],
middleware: [
searchesStateMiddleware,
copilotkitMiddleware,
stateStreamingMiddleware(
stateItem({
stateKey: "searches",
tool: "report_research_progress",
toolArgument: "searches",
}),
),
],
systemPrompt:
"You are a research assistant. Use report_research_progress " +
"to show each task and mark it done when complete.",
});Understand the two update phases#
The state-streaming middleware sends partial searches arguments while the model creates them. The frontend can show each partial value immediately.
The tool then returns a Command that saves the completed list. Its ToolMessage closes the active tool call.
Keep the state key, tool name, and tool argument identical. A mismatch sends updates to the wrong state field.
Render state in the UI#
Use the useAgent hook to access agent state anywhere in your app. You can render it in the chat, in dashboards, sidebars, or custom layouts.
import { useAgent } from "@copilotkit/react-core/v2";
function YourMainContent() {
const { agent } = useAgent({
agentId: "sample_agent",
});
const state = (agent.state ?? {}) as {
searches?: { query: string; done: boolean }[];
};
const searches = state.searches ?? [];
return (
<div>
{searches.map((search, index) => (
<div key={index}>
{search.done ? "✅" : "⏳"} {search.query}
</div>
))}
</div>
);
}Give it a try!#
Ask the agent to research a topic. The search items appear and update while the agent works.