---
title: Confirm before it runs
description: "requireConfirmation makes a tool wait for the person's own click on its control: the cursor parks on the button, and only a real click runs the tool."
sidebar:
  label: 3. Confirm before it runs
---

Anything the person would regret (an order, a booking, a deletion) should wait for their click. Set
**`requireConfirmation: true`**: the agent's call validates its input, the cursor flies to the tool's control and parks
there, and the tool runs only when the person clicks it.

## A complete example

**Vite**

```tsx title="src/components/place-order.tsx"
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
```

**Next.js**

```tsx title="components/place-order.tsx"
"use client";

import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
```

**TanStack Start**

```tsx title="src/components/place-order.tsx"
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
```

**Astro**

```tsx title="src/components/place-order.tsx"
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
```

The button reads "Place order · $28.00"; the cursor finds it by the tool's title, because a name that starts with the
title matches too.

## What the person sees

- **The cursor parks on the button**, with a label that says what to do: `Click "Place order · $28.00" to confirm`. The
  button gets a dashed outline (`[data-agent-active]` in `cursor.css`).
- **Their click runs the tool instead of the button's `onClick`**, so the order is placed once, by `execute`, and its
  return value is the agent's answer. Only a trusted click counts: a script calling `button.click()` cannot confirm
  for them.
- **Until they click, the agent waits.** The call does not time out while it is parked, and the chat shows it as
  waiting for the person. Moving the mouse or typing in a field does not cancel it; clicking or scrolling elsewhere does.

The input is validated before the cursor moves, so the person never approves a call that then fails its schema.

## How long it waits

The call waits 10 seconds for the click. After that it fails with `Error [needs_user]`, which tells the agent to ask
the person and call again, and the prompt leaves the page. It also ends early, with `Error [cancelled]`, when the
person moves on: they click anywhere but the control, scroll the page (wheel, touch, Page Down or Space), or start
another tool call themself (a button or chip that runs a tool, the devtools). Input inside the chat or the devtools
does not count, nor does a trackpad scroll still coasting from before the prompt appeared. An agent's own next call
never cancels it.

## It needs exactly one control

A confirm-gated tool **fails closed**. When there is no control to click (not on the page, disabled, or gone while it
waits), or when several controls match its name, the call fails with a `ConfirmationUnavailableError`: the agent reads
`Error [unavailable]` and a sentence that says why, and nothing runs unconfirmed.

Three product cards, each with an "Add to cart" button, all match the name "Add to cart". Give each button its own
accessible name, and the tool the same one:

```tsx
useWebTool({
  id: `add_to_cart_${product.sku}`,
  title: `Add to cart: ${product.name}`,
  requireConfirmation: true,
  // …
});

<Button type="submit" aria-label={`Add to cart: ${product.name}`}>
  Add to cart
</Button>
```

The visible text stays "Add to cart", and the name starts with it, so the cursor and a screen reader agree. A
[`target`](/docs/build/define-a-tool#when-the-control-depends-on-the-call) that picks one control per call works too. The
[devtools](/docs/build/devtools) show "3 matches" with a warning on a tool that would fail this way.

## Say what kind of tool it is

`requireConfirmation` changes what happens on the page. Flags say what the tool is, in plain words, for the agent to
read (they become WebMCP annotations); they change nothing on the page. A confirm-gated tool usually carries one:

| Flag | Meaning | Becomes |
|---|---|---|
| `readOnly` | Only reads. Safe to call at any time. | `readOnlyHint` |
| `destructive` | Deletes or overwrites; hard to undo. `clear_cart` sets it. | `destructiveHint` |
| `repeatable` | Calling twice has the same effect as once. | `idempotentHint` |
| `external` | Talks to something outside the page: the network, another system. `place_order` sets it. | `openWorldHint` |
| `safeResponse` | `false`: the answer may hold text the site did not write (reviews, messages), so the agent must not follow instructions in it. | `untrustedContentHint`, inverted |

Leave a flag out when it does not apply: "not said" is not the same as `false`.

For a tool that needs the person in another way ("which size?"), call
[`waitingForUser(toolName, prompt)`](/docs/reference/core/waiting-for-user) inside `execute`: the chat shows the prompt,
and the agent's tool time limit stops while the person answers.

## Try it

Render `<PlaceOrder />` on a page, then in the browser console:

```js
await agent.invoke("place_order");
```

The cursor parks on **Place order** and the promise waits. Click the button: the call resolves with "Order FS-…
placed, $28.00." Scroll or click while the cursor travels: it gets out of the way, and the
call still waits for your click.

## Options you'll use most

| Option | What it is for |
|---|---|
| `requireConfirmation` | Wait for the person's click on the tool's control. Default `false`. |
| `title` | The name of the one control that confirms. |
| `target` | Which control, when the title is not enough or several share it. |
| `destructive`, `external` | Tell the agent what kind of action it is about to ask for. |

Every option, with flags and annotations: [`defineWebTool`](/docs/reference/core/define-web-tool#flags-and-annotations).
