Skip to main content
When your agent is embedded on a website, it can answer questions, but it cannot take actions. The user asks “add this to my cart” and the agent says “you can use the Add to Cart button on the right.” That’s advice, not assistance. Page tools close this gap. The host page registers JavaScript handler functions as tools. The agent can call them when appropriate. The handler runs in the browser with full access to the application’s DOM, state, and APIs. The agent decides when to act; the host page’s handler decides how.

Why this matters

Without page tools, embedded agents are conversational. Users ask questions, get answers. With page tools, every embedded agent becomes an assistant that can operate the application. The user says “add this to my cart” and the item is in the cart. The user says “move this candidate to the interview stage” and the pipeline updates. The capabilities are yours to define. Every action the agent can take is one you deliberately enabled. Different pages can offer different tools. Admin users can get tools that regular users don’t. You shape the agent’s abilities to match your application. This is especially valuable for scenarios like e-commerce (add to cart, apply coupons, check stock, change shipping method, where the agent operates the store alongside the user), internal platforms (move pipeline candidates, update records, trigger workflows, where the agent participates in the user’s workflow instead of describing it), SPA navigation (guide the user to the right page or section without them hunting through menus), and form assistance (pre-fill fields, validate inputs, submit forms, where the agent handles the tedious parts).

When to use page tools

Use them when the agent needs to do something on the host page, not just know something about it. Page tools are the right choice when:
  • The action happens in the browser (DOM manipulation, client-side API calls, navigation)
  • The handler needs access to the host page’s authenticated session, state, or routing
  • The capability should be dynamic (different tools on different pages, for different users)
Page tools are not the right choice when:
  • The action requires server-side credentials or infrastructure (use an MCP server instead)
  • The result needs rich visual rendering via the widget pipeline (widgets require the proxy)
  • The data should flow to the agent’s system prompt for reasoning (use Situational Awareness)
For many scenarios, page tools and situational awareness work together: SA tells the agent what the user is looking at, and a page tool lets the agent act on it.

How page tools compare to other data channels

The platform has several channels for passing data between the host page and the agent. Each has a distinct purpose:
Page toolsSituational awarenessPass-through parameters
DirectionAgent to host pageHost page to agentHost page to tools
PurposeLet the agent take actions on the pageGive the agent context to reason aboutGive tools exact data without LLM involvement
Who sees itAgent calls it; handler runs in browserThe LLM (in its system prompt)The MCP server only
When setAny time after the element registersPer messagePer message
Typical contentHandler functions for app actionsPage title, user role, priming questionJSON payloads, document content

Registering tools

Call agent.setPageTools() with an array of tool definitions:
const agent = document.querySelector('mindset-agent');

agent.setPageTools([{
  name: 'add_to_cart',
  description: 'Add the product the user is currently viewing to their shopping cart.',
  runningDescription: 'Adding to cart...',
  completedDescription: 'Added to cart',
  parameters: {
    type: 'object',
    properties: {
      quantity: { type: 'number', description: 'Number to add (default 1)' },
    },
  },
  handler: async (args) => {
    await myApp.cart.addItem(currentProductId, args.quantity ?? 1);
    const cart = await myApp.cart.getState();
    return { success: true, cartTotal: cart.formattedTotal };
  },
}]);
Each tool needs:
FieldRequiredTypePurpose
nameYesstringUnique tool name (across all tools the agent has, including MCP tools)
descriptionYesstringThe LLM reads this to decide when to call the tool. Write it for the agent, not for a developer.
parametersYesJSON Schema objectInput parameter definitions
handlerYesasync (args) => resultBrowser-local function that executes the action
runningDescriptionNostring | Record<string, string>Status indicator text while the tool runs. Defaults to “Running …”
completedDescriptionNostring | Record<string, string>Status indicator text after the tool completes. Defaults to ” completed”
agent.setPageTools() is sync. Call it once mindset.init() has completed and the element has fired mindset:agent-idle. See § When the element is ready to call. You don’t need to await anything.

Updating tools dynamically

As the user navigates, the available actions change. Call agent.setPageTools() to replace the tool set at any point. The agent picks up the new tools on the next message.
// Product page
agent.setPageTools([
  { name: 'add_to_cart', description: 'Add the current product to the cart.', /* ... */ },
  { name: 'check_stock', description: 'Check stock for a size and color.', /* ... */ },
]);

// User navigates to checkout
agent.setPageTools([
  { name: 'apply_coupon', description: 'Apply a discount coupon code.', /* ... */ },
  { name: 'change_shipping', description: 'Change the shipping method.', /* ... */ },
]);
setPageTools() replaces the entire tool set each time. To clear all page tools, pass an empty array.

Multilingual status descriptions

The SDK detects the user’s browser language automatically. Pass a string for English only, or a record keyed by language code for multilingual support:
{
  name: 'add_to_cart',
  description: 'Add the current product to the shopping cart.',
  runningDescription: {
    en: 'Adding to cart...',
    nl: 'Toevoegen aan winkelwagen...',
    fr: 'Ajout au panier...',
  },
  completedDescription: {
    en: 'Added to cart',
    nl: 'Toegevoegd aan winkelwagen',
    fr: 'Ajouté au panier',
  },
  // ...
}
The SDK resolves the user’s language, falls back to English if the requested language is not provided, and falls back to the generated default if neither is present.

Combining with situational awareness

Page tools and situational awareness are complementary. SA tells the agent what the user is looking at. Page tools tell the agent what it can do about it.
// Context: what the user sees
agent.setSituationalAwareness({
  page: 'Product Detail',
  productName: 'Industrial Widget X-500',
  productPrice: '$120',
  stockStatus: 'In Stock',
});

// Capability: what the agent can do
agent.setPageTools([{
  name: 'add_to_cart',
  description: 'Add the product the user is viewing to their cart.',
  runningDescription: 'Adding to cart...',
  completedDescription: 'Added to cart',
  parameters: {
    type: 'object',
    properties: {
      quantity: { type: 'number', description: 'Number to add (default 1)' },
    },
  },
  handler: async (args) => {
    await myApp.cart.addItem('X500', args.quantity ?? 1);
    const cart = await myApp.cart.getState();
    return { success: true, cartTotal: cart.formattedTotal };
  },
}]);
The agent knows which product because of SA. It knows how to add it because you registered the tool. On a page where add_to_cart hasn’t been registered, the agent naturally stays conversational (“You can add it to your cart using the button on the right”).

Scenarios

E-commerce: product actions

agent.setPageTools([
  {
    name: 'add_to_cart',
    description: 'Add the current product to the shopping cart.',
    runningDescription: 'Adding to cart...',
    completedDescription: 'Added to cart',
    parameters: {
      type: 'object',
      properties: {
        quantity: { type: 'number', description: 'Number to add (default 1)' },
      },
    },
    handler: async (args) => {
      await myApp.cart.addItem(currentProductId, args.quantity ?? 1);
      const cart = await myApp.cart.getState();
      return { success: true, cartTotal: cart.formattedTotal, itemCount: cart.items.length };
    },
  },
]);
User: “Add this to my cart” Agent: “Done. I’ve added the Industrial Widget X-500 to your cart. Your total is now $2,460 with 4 items.”

HR platform: pipeline management

agent.setPageTools([{
  name: 'move_candidate',
  description: 'Move a candidate to a different stage in the hiring pipeline.',
  runningDescription: 'Moving candidate...',
  completedDescription: 'Candidate moved',
  parameters: {
    type: 'object',
    properties: {
      stage: {
        type: 'string',
        enum: ['screening', 'interview', 'offer', 'hired'],
        description: 'Target pipeline stage',
      },
    },
    required: ['stage'],
  },
  handler: async (args) => {
    const result = await myApp.pipeline.moveCandidate(currentCandidateId, args.stage);
    return { success: true, candidateName: result.name, newStage: result.stage };
  },
}]);
User: “Move this candidate to the interview stage” Agent: “Done. I’ve moved Sarah to the Interview stage. The hiring manager has been notified.”

Destructive actions: confirmation dialog

There’s no platform-level confirmation mechanism. If a tool has real-world consequences, build confirmation into your handler:
handler: async (args) => {
  const confirmed = await myApp.showConfirmation(
    'Delete this record? This cannot be undone.'
  );
  if (!confirmed) {
    return { success: false, reason: 'User declined' };
  }
  await myApp.records.delete(currentRecordId);
  return { success: true };
}
You own the confirmation UI. The agent waits for the handler to resolve, however long that takes.

Writing good tool descriptions

The description field is the single most important field. It’s what the agent reads to decide whether to call the tool and when.
// Bad: too vague
{ description: 'Cart operations' }

// Bad: too technical
{ description: 'POST /api/v2/cart/items with productId and quantity fields' }

// Good: clear intent
{ description: 'Add a product to the shopping cart. Use when the user wants to buy something or add an item.' }

Handling errors

Return error information in the result object. The agent reads it and communicates the failure naturally:
handler: async (args) => {
  try {
    await myApp.cart.addItem(args.productId, args.quantity);
    return { success: true };
  } catch (err) {
    return { success: false, error: err.message };
  }
}
Never throw from a handler. Always return a result object. The platform wraps handler invocations in a safety net (try/catch with a 30-second timeout), but a structured error from your code will always produce a better experience than a generic failure message. If a handler does throw despite this guidance, the SDK catches the exception and the tool output passed to the agent will likely contain an error. The agent will typically acknowledge the failure in conversation and the turn continues normally. Custom UIs rendering tool output can detect an error field in mindset:tool-end.detail.output to surface a “tool failed” indicator. Returning a structured { success: false, error } from your handler remains the cleaner path (you control the exact message), but unhandled exceptions don’t break the turn.

Security

The agent proposes actions. Your handler decides whether to execute them. The agent has no direct access to the DOM, your APIs, your cookies, or your session. It can only invoke handlers you registered. Your handler is the gatekeeper:
handler: async (args) => {
  if (!currentUser.permissions.includes('cart.write')) {
    return { success: false, error: 'You do not have permission to modify the cart.' };
  }
  if (args.quantity > 10) {
    return { success: false, error: 'Maximum 10 items per order.' };
  }
  await myApp.cart.addItem(productId, args.quantity ?? 1);
  return { success: true };
}
Treat handler arguments as untrusted input. The LLM generates the args object. It’s no more trusted than user input from a form field. Validate and sanitize the same way you would for any external input. Prompt injection can trigger tool calls. An attacker who controls content the agent reads could instruct it to call your tools with specific arguments. Build handlers defensively: check permissions, validate arguments, use confirmation dialogs for destructive operations.

Things to know

Call setPageTools() on every navigation. Handlers are closures. They capture the surrounding application state at the time they’re defined. If the user navigates but you don’t call setPageTools() again, the handlers reference stale state. Pair setPageTools() with setSituationalAwareness() in your navigation handler. Don’t tear down the page from a handler. If the handler does window.location.href = args.path, it tears down the SDK mid-execution. The tool result never reaches the agent. Use the browser History API for SPA navigation, or return a result and let the page navigate after the turn completes. Return JSON-serializable objects. Returning a DOM element, a circular reference, or a function will produce a generic error. Stick to plain objects with string, number, boolean, and array values.

Design properties

Page tools are dynamic per message. They are read before each message send, so the agent’s available actions evolve with the page state. The tool list is flat. The agent sees page tools identically to MCP tools, with no special handling and no separate category. Page tools are always allowed. They bypass the directive tool filter because they’re customer-registered, not agent-config-controlled. If you registered it, the agent can use it. Execution is local. Handlers run in the browser, in the user’s authenticated session, with no proxy and no server round-trip. The platform provides a safety net. Every invocation is wrapped with try/catch, a 30-second timeout, and a JSON serialization guard. You don’t need to handle these yourself.

Quick reference

ConceptDescription
agent.setPageTools([...])Register or replace tools dynamically
PageToolDefinitionType: { name, description, parameters, handler, runningDescription?, completedDescription? }
runningDescriptionStatus text while tool runs. String (English) or Record<string, string> (multilingual).
completedDescriptionStatus text after tool completes. Same type.
Directive filterPage tools always bypass it (always available)
Timeout30 seconds, then error result
ExecutionLocal (browser), no proxy involvement

Situational awareness

Give the agent context to reason about.

Pass-through parameters

Inject exact data into tool calls without the LLM copying it.

Data channels overview

How page tools fit alongside the other channels.

Element methods

Full element method surface.

DOM events

Tool-call events (mindset:tool-start, mindset:tool-end).