showcase/shell-docs/src/content/reference/angular/functions/registerFrontendTool.mdx
registerFrontendTool registers a client-side tool with CopilotKit. When the agent decides to call the tool, the provided handler function executes in the browser, and the value it resolves to is surfaced back to the agent as the tool result. Optionally, you can supply a component (a standalone Angular component) to render custom UI in the chat that shows the tool call's progress and result.
You call registerFrontendTool inside an Angular injection context (a component or service constructor, or a field initializer). The tool registers immediately, and CopilotKit removes it automatically when the owning component or service is destroyed. Parameter schemas are defined with a Standard Schema such as a Zod object, which is used for both validation and type inference of the handler's arguments.
This is the Angular equivalent of React's useFrontendTool(). The handler runs inside the same injector that registered the tool, so it can call inject() to reach your other Angular services.
import { registerFrontendTool } from "@copilotkit/angular";
function registerFrontendTool<Args extends Record<string, unknown>>(
frontendTool: FrontendToolConfig<Args>,
): void;
<PropertyReference name="handler" type="(args: Args, context: FrontendToolHandlerContext) => Promise<unknown>" required
An async function that runs when the agent calls the tool. It receives the
validated, typed `args` and a `context` object, and runs inside the
injection context that registered the tool, so you can call `inject()`
inside it. The resolved value is returned to the agent as the tool result.
<PropertyReference name="context.toolCall" type="ToolCall">
The raw AG-UI tool call metadata, including the tool call `id`.
</PropertyReference>
<PropertyReference name="context.agent" type="AbstractAgent">
The agent instance that invoked the tool.
</PropertyReference>
<PropertyReference name="context.signal" type="AbortSignal">
An `AbortSignal` that is aborted when the user stops the agent. Long
running handlers can check `signal.aborted` or pass `signal` to `fetch`
to cancel cooperatively.
</PropertyReference>
registerFrontendTool returns void. It performs the registration as a side effect and wires up automatic cleanup.
handler is invoked with runInInjectionContext using the injector that registered the tool, so inject() works inside it.name and agentId you registered with. There is no manual unregister step.component, it is registered alongside the tool so the chat can render the tool call's progress and result.Register the tool in the component constructor. The handler reads and writes a local signal, then returns a string result for the agent.
import { Component, signal } from "@angular/core";
import { z } from "zod";
import { registerFrontendTool } from "@copilotkit/angular";
@Component({
selector: "app-todo",
standalone: true,
template: `
<ul>
@for (todo of todos(); track todo) {
<li>{{ todo }}</li>
}
</ul>
`,
})
export class TodoComponent {
readonly todos = signal<string[]>([]);
constructor() {
registerFrontendTool({
name: "addTodo",
description: "Add a new item to the user's todo list",
parameters: z.object({
text: z.string().describe("The todo item text"),
priority: z.enum(["low", "medium", "high"]).describe("Priority level"),
}),
handler: async ({ text, priority }) => {
this.todos.update((current) => [...current, text]);
return `Added "${text}" with ${priority} priority`;
},
});
}
}
Because the handler runs in the injection context, you can call inject() for a service instead of capturing it from the component:
import { Component, inject } from "@angular/core";
import { z } from "zod";
import { registerFrontendTool } from "@copilotkit/angular";
import { WeatherService } from "./weather.service";
@Component({
selector: "app-weather",
standalone: true,
template: ``,
})
export class WeatherComponent {
constructor() {
registerFrontendTool({
name: "getWeather",
description: "Fetch weather information for a city",
parameters: z.object({
city: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
handler: async ({ city, units }, { signal }) => {
const weather = inject(WeatherService);
const result = await weather.fetch(city, units, { signal });
return JSON.stringify(result);
},
});
}
}
Pass a standalone component as component to visualize the tool call in chat. The renderer implements ToolRenderer<Args>, so it exposes a toolCall signal input that carries the status, the (possibly partial) args, and the result.
import { Component, input } from "@angular/core";
import { AngularToolCall, ToolRenderer } from "@copilotkit/angular";
type WeatherArgs = { city: string; units: "celsius" | "fahrenheit" };
@Component({
selector: "app-weather-tool-view",
standalone: true,
template: `
@let call = toolCall();
@if (call.status === "in-progress") {
<div class="animate-pulse">Fetching weather for {{ call.args.city }}...</div>
} @else if (call.status === "complete") {
@let data = parse(call.result);
<div class="rounded border p-4">
<h3>{{ data.city }}</h3>
<p>{{ data.temperature }} {{ data.units }}</p>
<p>{{ data.conditions }}</p>
</div>
}
`,
})
export class WeatherToolViewComponent implements ToolRenderer<WeatherArgs> {
readonly toolCall = input.required<AngularToolCall<WeatherArgs>>();
protected parse(result: string) {
return JSON.parse(result);
}
}
Register the tool with that component:
import { Component, inject } from "@angular/core";
import { z } from "zod";
import { registerFrontendTool } from "@copilotkit/angular";
import { WeatherService } from "./weather.service";
import { WeatherToolViewComponent } from "./weather-tool-view.component";
@Component({
selector: "app-weather",
standalone: true,
template: ``,
})
export class WeatherComponent {
constructor() {
registerFrontendTool({
name: "getWeather",
description: "Fetch and display weather information for a city",
parameters: z.object({
city: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
handler: async ({ city, units }, { signal }) => {
const result = await inject(WeatherService).fetch(city, units, { signal });
return JSON.stringify(result);
},
component: WeatherToolViewComponent,
});
}
}