Frontend Tools
Let your agent interact with and update your application's UI.
Open your coding agent in your project's folder, or in an empty folder for a new app.This runs in a coding agent on your computer.
"""Neutral assistant shared by every chat-UI and frontend-tool demo.The page's tools (``useFrontendTool``, ``useHumanInTheLoop``, ...) arrive witheach run; the adapter turns them into ``FunctionTool`` proxies that end therun and hand the call to the browser, so this agent declares none itself."""from agents import Agentfrom demo_agents._common import MODEL, chat_settingsSYSTEM_PROMPT = "You are a helpful, concise assistant."neutral_agent = Agent( name="assistant", instructions=SYSTEM_PROMPT, model=MODEL, model_settings=chat_settings(),)See this in Inspector
Open Inspector on localhost. Go to Agents, then Frontend Tools. Your tool and its schema are listed.
More detail: Inspector.
What 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#
Nothing to wire on the agent
On every run, the OpenAI Agents AG-UI adapter adds each tool the frontend registered to a per-request copy of the agent, so the agent declares no tools of its own for this. When the model calls one, the run stops there, because the browser owns that call. The browser runs the handler, and the next request carries the result as a tool message, which the model sees as the call's output before it continues.
If a frontend tool has the same name as one of the agent's own tools, the agent's tool wins and the frontend one is ignored.
Tell the model when to call it
This is the part that is easy to miss. The tool arrives on every run, but a model with no instruction about it will answer in prose and never call it. Name the tool in the instructions and say what it is for.
from agents import Agent
from demo_agents._common import MODEL, chat_settings
SYSTEM_PROMPT = """You are a data visualization assistant.
When the user asks for a chart, call `render_bar_chart` or `render_pie_chart`
with a concise title, short description, and a `data` array of
`{label, value}` items. Pick bar for comparisons over a small set of
categories; pick pie for composition / share-of-whole.
If the user names a chart subject but does NOT supply concrete numbers
(e.g. "show me a pie chart of website traffic by source"), do NOT ask
them for data. Invent plausible illustrative sample values yourself,
call the appropriate `render_*` tool immediately, and briefly note in
the follow-up that the values are illustrative samples. Always render
the chart on the first turn -- never reply with a clarifying question
asking for the data.
Keep chat responses brief -- let the chart do the talking."""
gen_ui_tool_based_agent = Agent(
name="chart_assistant",
instructions=SYSTEM_PROMPT,
model=MODEL,
model_settings=chat_settings(),
)
Backend tools in the same turn
A frontend tool call ends the run as soon as the model makes it. A backend
tool the model called in the same turn and that has not finished is
cancelled, and the next request answers it as not having run. Where that
matters, turn off parallel tool calls on the agent with
ModelSettings(parallel_tool_calls=False).
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" }; },Registering a list of tools#
useFrontendTool registers one tool per call, so it cannot be called in a loop
over a list whose length changes between renders. When the set of tools comes
from state, from props, or from a backend response, use
useFrontendTools instead. It takes an
array and runs a single effect over it, so the array can be empty on one render
and hold twenty entries on the next.
useFrontendTools(
reports.map((report) => ({
name: `open_${report.id}`,
description: `Open the ${report.title} report`,
handler: async () => navigate(`/reports/${report.id}`),
})),
[navigate],
);Tools that leave the array are unregistered, tools that join it are registered,
and a re-render that produces an equal list does not re-register anything. A
description built from your data stays current on its own. The second argument
is for values a handler closes over, such as navigate above.