Skip to main content

Overview

The useFrontendTool hook dynamically registers a frontend tool that AI agents can invoke. Frontend tools run in the browser and can access client-side state, interact with the UI, or perform client-side operations.

Usage

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function MyComponent() {
  useFrontendTool({
    name: "searchProducts",
    description: "Search the product catalog",
    parameters: z.object({
      query: z.string().describe("Search query"),
    }),
    handler: async ({ query }) => {
      const results = await searchAPI(query);
      return results;
    },
  });
  
  return <div>...</div>;
}

Type Signature

function useFrontendTool<T extends Record<string, unknown>>(
  tool: ReactFrontendTool<T>,
  deps?: ReadonlyArray<unknown>
): void

Parameters

tool
ReactFrontendTool<T>
required
The tool definition object with the following properties:
deps
ReadonlyArray<unknown>
Optional dependency array (like useEffect). When dependencies change, the tool is re-registered with updated configuration.
useFrontendTool(tool, [userId, permissions]);
The tool is automatically re-registered when tool.available changes, even without including it in deps.

Return Value

This hook does not return a value. The tool is registered with CopilotKit and automatically unregistered on component unmount.

Examples

Basic Tool

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function WeatherWidget() {
  useFrontendTool({
    name: "getWeather",
    description: "Get weather for a city",
    parameters: z.object({
      city: z.string().describe("City name"),
    }),
    handler: async ({ city }) => {
      const response = await fetch(`/api/weather?city=${city}`);
      const data = await response.json();
      return `Weather in ${city}: ${data.temperature}°C, ${data.conditions}`;
    },
  });
  
  return <div>Weather widget ready</div>;
}

Tool with Custom Rendering

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function ProductSearch() {
  useFrontendTool({
    name: "searchProducts",
    description: "Search for products in the catalog",
    parameters: z.object({
      query: z.string(),
      category: z.string().optional(),
    }),
    handler: async ({ query, category }) => {
      const results = await searchProducts(query, category);
      return JSON.stringify(results);
    },
    render: ({ args, status, result }) => {
      if (status === "inProgress") {
        return <div>Preparing search...</div>;
      }
      
      if (status === "executing") {
        return (
          <div className="search-loading">
            Searching for "{args.query}"
            {args.category && ` in ${args.category}`}...
          </div>
        );
      }
      
      const products = JSON.parse(result);
      return (
        <div className="product-results">
          <h3>Found {products.length} products</h3>
          <ul>
            {products.map(p => (
              <li key={p.id}>{p.name} - ${p.price}</li>
            ))}
          </ul>
        </div>
      );
    },
  });
  
  return <div>Product search enabled</div>;
}

Conditional Tool Availability

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function AdminTools({ user }) {
  const isAdmin = user.role === "admin";
  
  useFrontendTool({
    name: "deleteUser",
    description: "Delete a user account (admin only)",
    parameters: z.object({
      userId: z.string(),
    }),
    handler: async ({ userId }) => {
      await deleteUserAccount(userId);
      return `User ${userId} deleted`;
    },
    available: isAdmin, // Only available to admins
  }, [isAdmin]); // Re-register when admin status changes
  
  return <div>Admin tools {isAdmin ? "enabled" : "disabled"}</div>;
}

Agent-Specific Tool

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function ResearchTools() {
  // This tool is only available to the research agent
  useFrontendTool({
    name: "searchPapers",
    description: "Search academic papers",
    parameters: z.object({
      query: z.string(),
      year: z.number().optional(),
    }),
    handler: async ({ query, year }) => {
      const papers = await searchAcademicPapers(query, year);
      return papers;
    },
    agentId: "research-agent",
  });
  
  return <div>Research tools ready</div>;
}

Tool with Context

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function DatabaseTools() {
  useFrontendTool({
    name: "queryDatabase",
    description: "Query the database",
    parameters: z.object({
      query: z.string(),
    }),
    handler: async ({ query }, context) => {
      // Access the agent and tool call via context
      console.log("Called by agent:", context.agent.agentId);
      console.log("Tool call ID:", context.toolCall.id);
      
      const results = await runQuery(query);
      return results;
    },
  });
  
  return <div>Database tools ready</div>;
}

Dynamic Tool Configuration

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";
import { useState } from "react";

function ConfigurableSearch() {
  const [searchScope, setSearchScope] = useState("all");
  
  useFrontendTool({
    name: "search",
    description: `Search content (current scope: ${searchScope})`,
    parameters: z.object({
      query: z.string(),
    }),
    handler: async ({ query }) => {
      // Handler uses current searchScope value
      return await searchWithScope(query, searchScope);
    },
  }, [searchScope]); // Re-register when scope changes
  
  return (
    <div>
      <select value={searchScope} onChange={e => setSearchScope(e.target.value)}>
        <option value="all">All</option>
        <option value="docs">Documentation</option>
        <option value="code">Code</option>
      </select>
    </div>
  );
}

Tool without Follow-up

import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

function QuickActions() {
  useFrontendTool({
    name: "copyToClipboard",
    description: "Copy text to clipboard",
    parameters: z.object({
      text: z.string(),
    }),
    handler: async ({ text }) => {
      await navigator.clipboard.writeText(text);
      return "Copied!";
    },
    followUp: false, // Don't generate follow-up message
  });
  
  return <div>Quick actions ready</div>;
}

Behavior

Registration

  • Tools are registered when the component mounts
  • If a tool with the same name already exists for the same agent, it is overridden with a warning
  • Tools are automatically unregistered when the component unmounts

Rendering

  • If a render function is provided, it persists in the render registry even after unmount
  • This allows tool calls in chat history to continue rendering their custom UI
  • Each tool is identified by the combination of name and agentId

Re-registration

Tools are re-registered when:
  • The deps array changes
  • The tool.available property changes
  • The tool.name changes

Notes

If you’re passing an inline object to useFrontendTool, wrap it in useMemo to avoid unnecessary re-registrations:
const tool = useMemo(() => ({
  name: "myTool",
  // ... rest of tool config
}), [/* dependencies */]);

useFrontendTool(tool);
For tools registered at the provider level (via CopilotKitProvider props), use a stable array reference. For dynamic tools that change frequently, use useFrontendTool instead.