Use this file to discover all available pages before exploring further.
CopilotKit provides a full React chat runtime and pairs especially well with LangGraph when you want the agent to return structured UI payloads instead of only plain text. In this pattern, your LangGraph deployment serves both the graph API and a custom CopilotKit endpoint, while the frontend parses assistant messages into dynamic React components.This approach is useful when you want:
a ready-made chat runtime instead of wiring stream.messages yourself
a custom server endpoint that can add provider-specific behavior next to your deployed graph
structured generative UI rendered from a constrained component registry
For CopilotKit-specific APIs, UI patterns, and runtime configuration, see the
CopilotKit docs.
At a high level, CopilotKit sits between your React app and the LangGraph deployment. The frontend sends conversation state to a custom /api/copilotkit route mounted alongside the graph API, that route forwards the request to LangGraph, and the response comes back with both assistant messages and any structured UI payloads your component registry can render.
Deploy the graph as usual using LangSmith or using a LangGraph development server.
Extend the deployment with an HTTP app that mounts a CopilotKit route next to the graph API.
Wrap the frontend in CopilotKit and point it at that custom runtime URL.
Register dynamic UI components and parse assistant responses into those components at render time.
Extend the LangGraph deployment with a custom endpoint
The key idea is that the LangGraph deployment does not only serve graphs. It can also load an HTTP app, which lets you mount extra routes next to the deployment itself.In langgraph.json, point http.app at your custom app entrypoint:
Then create the Hono app and register the CopilotKit route:
app.ts
import { Hono } from "hono";import { registerCopilotKit } from "./copilotkit.js";export const app = new Hono();registerCopilotKit(app);
This custom app is the important extension point: it mounts a CopilotKit-aware runtime without replacing the underlying LangGraph deployment.Inside that route, create a CopilotRuntime and point it back at the deployed graph using LangGraphAgent:
The route adapter is only half of the TypeScript setup. Your LangChain agent also needs middleware that reads the forwarded output_schema and turns it into a structured responseFormat for the model:
agent.ts
import { createAgent, createMiddleware, toolStrategy } from "langchain";import { z } from "zod";import { deepSearchTool, searchWebTool } from "../tools/index.js";const contextSchema = z.object({ output_schema: z.unknown().optional(),});const structuredOutputMiddleware = createMiddleware({ name: "CopilotKitStructuredOutput", contextSchema, wrapModelCall: async (request, handler) => { const rawOutputSchema = getRuntimeOutputSchema(request.runtime); const schema = normalizeOutputSchema(rawOutputSchema); if (!schema) { return handler(request); } const responseFormat = toolStrategy( schema as unknown as Parameters<typeof toolStrategy>[0], { toolMessageContent: "Structured UI response generated.", }, ); return handler({ ...request, responseFormat, }); },});export const agent = createAgent({ model: process.env.COPILOTKIT_MODEL ?? "google_genai:gemini-3.1-pro-preview", contextSchema, middleware: [structuredOutputMiddleware], tools: [searchWebTool, deepSearchTool], systemPrompt: `You are a helpful UI assistant inspired by the CopilotKit Shadify example.Build rich visual responses with the available UI components when they add value.Only wrap actual UI layouts inside cards. Plain Markdown answers should stay as Markdown.Use rows for side-by-side layouts with at most two columns.Prefer simple, polished outputs over dense dashboards.When using charts, make labels and values concise and easy to read.When showing code, prefer the code_block component.When researching topics, use the available search tools first and then present the result cleanly.`,});function normalizeOutputSchema(value: unknown): Record<string, unknown> | null { let schema = value; if (typeof schema === "string") { try { schema = JSON.parse(schema); } catch { return null; } } if (!schema || typeof schema !== "object" || Array.isArray(schema)) { return null; } const normalized = { ...(schema as Record<string, unknown>) }; if (!normalized.title) { normalized.title = "CopilotKitStructuredOutput"; } if (!normalized.description) { normalized.description = "Structured response schema for the CopilotKit preview."; } return normalized;}function getRuntimeOutputSchema(runtime: { context?: { output_schema?: unknown }; configurable?: Record<string, unknown>;}): unknown { if (runtime.context?.output_schema !== undefined) { return runtime.context.output_schema; } const configurable = runtime.configurable; if (!configurable || typeof configurable !== "object" || Array.isArray(configurable)) { return undefined; } return configurable.output_schema;}
This middleware is what makes useAgentContext({ description: "output_schema", ... }) useful on the frontend. The CopilotKit runtime forwards the schema, and the agent turns it into the structured output contract the model must follow.The result is a clean separation of concerns:
LangGraph still owns graph execution and persistence
CopilotKit owns the chat-facing runtime contract
your custom endpoint glues them together inside one deployment
The component registry lives in useChatKit(). This is where you define the set of components the agent is allowed to emit, such as cards, rows, columns, charts, code blocks, and buttons.
import { s } from "@hashbrownai/core";import { exposeComponent, exposeMarkdown, useUiKit } from "@hashbrownai/react";import { Button } from "@/components/ui/button";import { Card } from "@/components/ui/card";import { CodeBlock } from "@/components/ui/code-block";import { Row, Column } from "@/components/ui/layout";import { SimpleChart } from "@/components/ui/simple-chart";export function useChatKit() { return useUiKit({ components: [ exposeMarkdown(), exposeComponent(Card, { name: "card", description: "Card to wrap generative UI content.", children: "any", }), exposeComponent(Row, { name: "row", props: { gap: s.string("Tailwind gap size") as never, }, children: "any", }), exposeComponent(Column, { name: "column", children: "any", }), exposeComponent(SimpleChart, { name: "chart", props: { labels: s.array("Category labels", s.string("A label")), values: s.array("Numeric values", s.number("A value")), }, children: false, }), exposeComponent(CodeBlock, { name: "code_block", props: { code: s.streaming.string("The code to display"), language: s.string("Programming language") as never, }, children: false, }), exposeComponent(Button, { name: "button", children: "text", }), ], });}
This registry becomes the contract between the agent and the UI. The model is not generating arbitrary JSX. It is generating structured data that must validate against the components and props you exposed.