Frontend Tools
Let your agent interact with and update your application's UI.
/** * LangGraph TypeScript agent backing the Frontend Tools (In-App Actions) demo. * * The demo is about frontend tools — the agent has no custom backend tools. * CopilotKit forwards the frontend tool schemas to the agent at runtime via * `state.copilotkit.actions`; the agent binds them when invoking the model, * and the handler executes in the browser. */// region: setupimport { RunnableConfig } from "@langchain/core/runnables";import { SystemMessage } from "@langchain/core/messages";import { MemorySaver, START, StateGraph } from "@langchain/langgraph";import { ChatOpenAI } from "@langchain/openai";import { convertActionsToDynamicStructuredTools, CopilotKitStateAnnotation,} from "@copilotkit/sdk-js/langgraph";// CopilotKit forwards frontend tools to the agent via// `state.copilotkit.actions`. `CopilotKitStateAnnotation` adds that// channel to your graph's state; `convertActionsToDynamicStructuredTools`// turns the forwarded action schemas into LangChain tools you can bind// at model-invocation time.const AgentStateAnnotation = CopilotKitStateAnnotation;export type AgentState = typeof AgentStateAnnotation.State;const SYSTEM_PROMPT = "You are a helpful, concise assistant.";async function chatNode(state: AgentState, config: RunnableConfig) { const model = new ChatOpenAI({ temperature: 0, model: "gpt-4o-mini" }); const modelWithTools = model.bindTools!([ ...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []), ]); const response = await modelWithTools.invoke( [new SystemMessage({ content: SYSTEM_PROMPT }), ...state.messages], config, ); return { messages: response };}const workflow = new StateGraph(AgentStateAnnotation) .addNode("chat_node", chatNode) .addEdge(START, "chat_node") .addEdge("chat_node", "__end__");const memory = new MemorySaver();export const graph = workflow.compile({ checkpointer: memory,});// endregionWhat is this?#
Frontend tools let your agent define and invoke client-side functions that run entirely in the user's browser. Because the handler executes on the frontend, it has direct access to component state, browser APIs, and any third-party UI library the page already uses. That's how an agent can "reach into" the app: update React state, trigger animations, read localStorage, pop a toast, or steer the user's view.
This page covers the "agent drives the UI" shape of frontend tools. The same primitive also powers Generative UI and Human-in-the-loop; see those pages for interaction patterns.
When should I use this?#
Use frontend tools when your agent needs to:
- Read or modify React component state
- Access browser APIs like
localStorage,sessionStorage, or cookies - Trigger UI updates, animations, or transitions
- Show alerts, toasts, or notifications
- Interact with third-party frontend libraries
- Perform anything that requires the user's immediate browser context
How it works in code#
Register a frontend tool with useFrontendTool. Give it a name, a Zod schema for parameters, and a handler. The agent can then call it like any other tool and your frontend runs it in the browser.
import React, { useState } from "react";import { CopilotKit, CopilotSidebar, useFrontendTool,} from "@copilotkit/react-core/v2";import { z } from "zod";import { Background, DEFAULT_BACKGROUND } from "./background";import { useFrontendToolsSuggestions } from "./suggestions";function Chat() { const [background, setBackground] = useState<string>(DEFAULT_BACKGROUND); useFrontendTool({ name: "change_background", description: "Change the page background. Accepts any valid CSS background value — colors, linear or radial gradients, etc.", parameters: z.object({ background: z .string() .describe("The CSS background value. Prefer gradients."), }), handler: async ({ background }) => { setBackground(background); return { status: "success" }; }, });The handler receives the parsed, type-safe parameters and can do anything the browser can: update state, call an API, touch the DOM. Its return value is sent back to the agent as the tool result so the model can reason about what happened.
handler: async ({ background }) => { setBackground(background); return { status: "success" }; },