Search documentation

Search documentation

Customize your docs experience — choose your preferred framework for code examples:

Setting Up Tools

This guide walks through implementing client-side tools in your application. For a conceptual overview of what tools are and why they matter, see Tools.

Looking for server-side tools? If your tool needs database access, API keys, or backend logic, see the Server SDKs documentation.

Registering tools

Co-locate your tool metadata with its handler — the CLI automatically discovers these definitions via AST scanning.

examples/guides/tools/use-pillar-tool-basic.tsx
import { usePillarTool } from "@pillar-ai/react";
import { useRouter } from "next/navigation";
export function usePillarTools() {
const router = useRouter();
// Basic navigation tool
usePillarTool({
name: "open_dashboard",
type: "navigate",
description: "Navigate to the main dashboard",
examples: ["go to dashboard", "show me the dashboard", "open home"],
execute: () => {
router.push("/dashboard");
},
});
// Tool with input schema for data extraction
usePillarTool({
name: "view_user_profile",
type: "navigate",
description: "View a specific user's profile page",
inputSchema: {
type: "object",
properties: {
userId: { type: "string", description: "The user ID to view" },
},
required: ["userId"],
},
examples: ["show user 123's profile", "view profile for john@example.com"],
execute: ({ userId }) => {
router.push(`/users/${userId}`);
},
});
// Auto-run tool (executes without confirmation)
usePillarTool({
name: "toggle_dark_mode",
type: "trigger_tool",
description: "Toggle between light and dark theme",
autoRun: true,
examples: ["switch to dark mode", "turn on light theme"],
execute: () => {
document.documentElement.classList.toggle("dark");
},
});
}

Multiple Tools

Register multiple related tools in a single call:

examples/guides/tools/use-pillar-tool-multiple.tsx
import { usePillarTool } from "@pillar-ai/react";
import { useRouter } from "next/navigation";
export function useNavigationTools() {
const router = useRouter();
// Register multiple related tools in a single call
usePillarTool([
{
name: "open_billing",
type: "navigate",
description: "Navigate to billing and subscription settings",
examples: ["go to billing", "view my subscription", "payment settings"],
execute: () => router.push("/settings/billing"),
},
{
name: "open_team",
type: "navigate",
description: "Navigate to team management page",
examples: ["manage team", "invite team members", "team settings"],
execute: () => router.push("/settings/team"),
},
{
name: "open_integrations",
type: "navigate",
description: "Navigate to integrations and connected apps",
examples: ["view integrations", "connect apps", "api settings"],
execute: () => router.push("/settings/integrations"),
},
]);
}

The CLI scanner finds both usePillarTool / injectPillarTool and defineTool calls when syncing.

Tool Types

TypeDescriptionUse Case
navigateNavigate to a page in your appSettings, dashboard, detail pages
trigger_toolRun custom logicToggle features, API calls, custom workflows
queryFetch data and return to the AI agentList items, validate queries, inspect state
inline_uiShow interactive UI in chatForms, confirmations, previews
open_modalOpen a modal or dialogConfirmation forms, settings dialogs
fill_formPre-fill form fieldsInvite forms, transfer forms
external_linkOpen URL in new tabDocumentation, external resources
copy_textCopy text to clipboardAPI keys, code snippets
start_tutorialStart a walkthroughOnboarding tours

Tool Properties

examples/guides/tools/tool-properties.ts
usePillarTool({
// Required
name: string, // Unique identifier for this tool
description: string, // AI uses this to match user intent
// Required for non-inline_ui types
execute: (input) => void, // Your execution logic
// Required for inline_ui type (instead of execute)
render: Component, // React/Vue/Angular component to render in chat
// Optional
type?: ToolType, // navigate, trigger_tool, query, inline_ui, external_link, copy_text
guidance?: string, // Per-tool instructions for the AI agent
examples?: string[], // Example phrases that trigger this tool
inputSchema?: { // JSON Schema for extracting data from user queries
type: "object",
properties: Record<string, unknown>,
required?: string[],
},
outputSchema?: { // JSON Schema for output fields (supports sensitive: true)
type: "object",
properties: Record<string, unknown>,
},
autoRun?: boolean, // Execute without user clicking (default: false)
autoComplete?: boolean, // Mark done immediately (default: true)
needsConfirmation?: boolean, // Show Confirm/Cancel UI before execute (default: false)
renderConfirmation?: Component, // Custom confirmation UI (implies needsConfirmation)
requiredContext?: object, // Context required for tool to be available
webMCP?: boolean, // Also register with WebMCP (navigator.modelContext)
});

To expose a tool to browser agents via WebMCP, set webMCP: true in the tool definition. Registration only occurs in environments where navigator.modelContext is available.

Handler Return Values

The execute handler returns data to the agent. Throw for errors:

examples/guides/tools/handler-return-values.tsx
usePillarTool({
name: "my_tool",
description: "Example tool showing return values",
execute: async () => {
// Success — return flat data (what you return is what the agent sees)
return { message: "Invite sent!" };
// Failure — throw an error (SDK catches it and reports to the agent)
throw new Error("Something went wrong");
},
});

Wizard Tools

For tools that open multi-step flows (wizards, modals with multiple steps), signal completion when the user finishes:

examples/guides/tools/wizard-complete-tool.tsx
import { usePillarTool, usePillar } from "@pillar-ai/react";
export function useOnboardingTool() {
const { completeTool } = usePillar();
usePillarTool({
name: "start_onboarding",
type: "trigger_tool",
description: "Start the onboarding wizard",
execute: () => {
openOnboardingWizard({
onComplete: () => {
// Signal to Pillar that the tool is done
completeTool();
},
});
// Don't return success yet - wizard handles completion
},
});
}

For simple tools (navigation, toggles), the SDK handles completion automatically when your handler returns. You only need to call completeTool() for flows where the SDK can't know when the user is "done"—like multi-step wizards or forms that require user input.

If the agent may call multiple form-opening tools in one response, see Sequential Tool Execution below for how to queue them.

Sequential Tool Execution

When a user says something like "Create a billing trigger, an escalation trigger, and a refund macro," the agent calls three form-opening tools. Each tool navigates to a different form — but only the last navigation survives, because they all fire before the user has submitted anything.

The fix has two parts:

  1. Defer completion — don't return from execute or call completeTool() until the user submits the form (covered in Wizard Tools above)
  2. Queue forms — only open one form at a time, advancing to the next after submit or cancel
examples/guides/tools/sequential-form-queue.tsx
import { useRef, useCallback } from "react";
import { usePillarTool, usePillar } from "@pillar-ai/react";
import { useNavigate } from "react-router-dom";
// Step 1: Create a queue hook that serializes form-opening tools
interface QueuedForm {
toolName: string;
data: Record<string, unknown>;
open: () => void;
}
export function useFormQueue() {
const queue = useRef<QueuedForm[]>([]);
const active = useRef<QueuedForm | null>(null);
const { completeTool } = usePillar();
const processNext = useCallback(() => {
if (queue.current.length === 0) {
active.current = null;
return;
}
const next = queue.current.shift()!;
active.current = next;
next.open();
}, []);
const enqueue = useCallback(
(toolName: string, data: Record<string, unknown>, open: () => void) => {
const item: QueuedForm = { toolName, data, open };
if (!active.current) {
active.current = item;
item.open();
} else {
queue.current.push(item);
}
},
[]
);
const onSubmit = useCallback(
(resultData?: Record<string, unknown>) => {
if (!active.current) return;
completeTool(active.current.toolName, true, resultData);
processNext();
},
[completeTool, processNext]
);
const onCancel = useCallback(() => {
if (!active.current) return;
completeTool(active.current.toolName, false);
processNext();
}, [completeTool, processNext]);
return { enqueue, onSubmit, onCancel };
}
// Step 2: Register tools that enqueue instead of navigating directly
export function useSetupTools() {
const navigate = useNavigate();
const { enqueue } = useFormQueue();
usePillarTool([
{
name: "open_create_trigger",
type: "trigger_tool",
description: "Open the trigger creation form pre-filled with the given data.",
execute: (data) => {
enqueue("open_create_trigger", data, () => {
navigate(`/triggers/new?prefill=${encodeURIComponent(JSON.stringify(data))}`);
});
// No return — completion is deferred to form submit
},
},
{
name: "open_create_macro",
type: "trigger_tool",
description: "Open the macro creation form pre-filled with the given data.",
execute: (data) => {
enqueue("open_create_macro", data, () => {
navigate(`/macros/new?prefill=${encodeURIComponent(JSON.stringify(data))}`);
});
},
},
]);
}
// Step 3: Wire form submit/cancel to the queue
function CreateTriggerPage() {
const { onSubmit, onCancel } = useFormQueue();
const handleSubmit = (formData: Record<string, unknown>) => {
const trigger = saveTrigger(formData);
onSubmit({ id: trigger.id, name: trigger.name });
};
return <TriggerForm onSubmit={handleSubmit} onCancel={onCancel} />;
}

The queue ensures each form gets its turn. On submit, completeTool() reports success to the agent and the next queued form opens automatically. On cancel, the tool reports failure and the queue still advances — no deadlocks.

Data Extraction

Define an inputSchema to have the AI extract structured data from user messages:

examples/guides/tools/data-extraction-schema.tsx
usePillarTool({
name: "add_source",
type: "trigger_tool",
description: "Add a new knowledge source",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL of the source to add" },
name: { type: "string", description: "Display name for the source" },
},
required: ["url", "name"],
},
execute: ({ url, name }) => {
openAddSourceWizard({
prefillUrl: url,
prefillName: name,
});
},
});

Now when a user says "Add my docs site at docs.example.com", the AI extracts the URL and passes it to your handler.

Query Tools

Use type: 'query' for tools that fetch data and return it to the AI agent. The return value of execute is sent back to the agent for further reasoning.

tsx
usePillarTool({
name: 'list_projects',
description: 'List all projects the user has access to',
type: 'query',
autoRun: true,
autoComplete: true,
execute: async () => {
const projects = await api.getProjects();
return projects.map(p => ({ id: p.id, name: p.name, status: p.status }));
},
});

Query tools enable the verify-then-commit pattern: the agent fetches data first, inspects the result, and only commits changes when the data looks right. See Agent Guidance for how to configure multi-step workflows.

All tool types return data to the agent — whatever your execute handler returns is what the agent sees. This applies to trigger_tool, navigate, and every other type, not just query.

Confirmation UI

Use needsConfirmation: true for tools that perform destructive or irreversible actions. When the agent calls the tool, the SDK shows a Confirm/Cancel UI instead of executing immediately.

examples/guides/tools/confirmation-basic.tsx
import { usePillarTool } from "@pillar-ai/react";
usePillarTool({
name: "delete_project",
description: "Permanently delete a project and all its data",
type: "trigger_tool",
needsConfirmation: true,
inputSchema: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID to delete" },
},
required: ["projectId"],
},
execute: async ({ projectId }) => {
await api.deleteProject(projectId);
return { deleted: true };
},
});

The user must click Confirm before execute runs. If they click Cancel, the tool is dismissed without executing.

For custom confirmation UI, use renderConfirmation instead of the default buttons. Providing renderConfirmation implies needsConfirmation — you don't need to set both.

examples/guides/tools/confirmation-custom.tsx
import { usePillarTool, type ConfirmationRenderProps } from "@pillar-ai/react";
function ConfirmPurchase({
data,
onConfirm,
onCancel,
isReady,
isLatest,
}: ConfirmationRenderProps<{ total: number; cartId: string }>) {
return (
<div className={`p-4 border rounded ${!isLatest ? "opacity-50" : ""}`}>
<p>Complete purchase for ${data.total}?</p>
<div className="flex gap-2 mt-4">
<button onClick={() => onConfirm()} disabled={!isReady}>
Buy Now
</button>
<button onClick={onCancel}>Cancel</button>
</div>
</div>
);
}
usePillarTool({
name: "complete_purchase",
description: "Complete the purchase",
type: "trigger_tool",
renderConfirmation: ConfirmPurchase,
execute: async ({ cartId }) => {
await api.checkout(cartId);
return { success: true };
},
});

Confirmation component props

Your confirmation component receives the following props. The shape is the same across React, Vue, and Angular (Angular uses @Input() and @Output() decorators instead of function props).

PropTypeDescription
dataTData the AI extracted from the conversation via inputSchema
onConfirm(modifiedData?) => voidApprove the action. Optionally pass modified data to override what the AI sent to execute.
onCancel() => voidDismiss the confirmation without executing
isReadybooleanTrue when the AI is not streaming — safe to interact (React only)
isLatestbooleanTrue when this is the most recent card in the chat (React only)

Tool Guidance

The guidance field gives the AI agent per-tool usage instructions — how to use the tool, what to do before/after, and common pitfalls. It's different from description, which is only for intent matching.

tsx
usePillarTool({
name: 'create_chart',
description: 'Create a chart on a dashboard',
guidance:
'Always test the query with test_datasource_query first. ' +
'Only create the chart after the query returns valid data.',
type: 'trigger_tool',
execute: async (data) => { /* ... */ },
});

For cross-tool workflows and global agent instructions, see Agent Guidance.

Context-Based Tool Filtering

Add requiredContext to a tool so it's only suggested when the user's context matches — useful for admin-only tools, feature-gated tools, or page-specific tools.

tsx
usePillarTool({
name: 'manage_users',
description: 'Manage team members and roles',
type: 'trigger_tool',
requiredContext: { userRole: 'admin' },
execute: async () => { /* ... */ },
});

See Adding User Context for the full context API, how to set context in your app, and how filtering works.

Best Practices

Write Clear Descriptions

The AI matches user queries to your tool descriptions. Be specific:

examples/guides/tools/good-descriptions.tsx
// Good - specific about when to use
usePillarTool({
name: "view_billing",
description: "Navigate to billing page. Suggest when user asks about payments, invoices, or subscription.",
// ...
});
// Less helpful - too generic
usePillarTool({
name: "view_billing",
description: "Go to billing",
// ...
});

Add Example Phrases

Help the AI understand different ways users might phrase requests:

examples/guides/tools/example-phrases.tsx
usePillarTool({
name: "invite_member",
description: "Invite a new team member",
examples: [
"invite someone to my team",
"add a new team member",
"how do I add users?",
"share access with someone",
],
execute: () => setShowInviteModal(true),
});

Handle Errors Gracefully

examples/guides/tools/error-handling.tsx
usePillarTool({
name: "export_data",
type: "trigger_tool",
description: "Export all data as CSV",
execute: async () => {
await exportData();
return { message: "Export complete" };
// If exportData() throws, the SDK catches it and
// reports the error to the agent automatically.
},
});

Close Panel When Appropriate

For tools that take over the screen:

examples/guides/tools/close-panel.tsx
import { usePillarTool, usePillar } from "@pillar-ai/react";
export function useTourTool() {
const { close } = usePillar();
usePillarTool({
name: "start_tour",
type: "trigger_tool",
description: "Start the onboarding tour",
execute: () => {
close();
startOnboardingTour();
},
});
}

Generic Event Handlers

For tools that don't fit the usePillarTool pattern (e.g., handling events from the backend, generic routing), you can use pillar.on() or pillar.onTask():

examples/guides/tools/provider-on-task.tsx
import { usePillar } from "@pillar-ai/react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
export function useGenericHandlers() {
const { on, onTask } = usePillar();
const router = useRouter();
useEffect(() => {
// Handle any navigate tool dynamically
onTask("navigate", (data) => {
router.push(data.path);
});
// Handle backend-triggered events
on("session:updated", (session) => {
console.log("Session updated:", session);
});
}, [on, onTask, router]);
}

This is useful for:

  • Handling dynamic tool names
  • Responding to backend-triggered events
  • Legacy handler patterns during migration

Next Steps