---
title: Tools with input
description: A form is a tool with an inputSchema. The cursor types the agent's values into your form's fields, your form library hears it, and fill shows what it cannot type.
sidebar:
  label: 2. Tools with input
---

A form is not a special kind of tool. It is **a tool with an `inputSchema`**: the agent sends values, the cursor types
each one into the field it belongs to, and your form library hears the typing like a person's. `execute` is the
function the form already submits to. There is no `form` option, no adapter and no second hook.

## A complete example

shadcn/ui's `Form` on React Hook Form, with one zod schema for the agent's call and the person's submit. To follow
along, add the components this page uses (`form` brings React Hook Form, its zod resolver and zod with it):

```package-install
npx shadcn@latest add form input select label
```

**Vite**

```tsx title="src/components/add-to-cart.tsx"
import { zodResolver } from "@hookform/resolvers/zod";
import { useWebTool } from "@webmcp-agents/react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { cart } from "@/lib/cart";

const SIZES = ["S", "M", "L"] as const;

// One schema checks the agent's call and the person's submit.
const schema = z.object({
  size: z.enum(SIZES).describe("The size: S, M or L"),
  quantity: z
    .number()
    .int()
    .min(1)
    .max(10)
    .describe("How many to add, e.g. 2"),
});

type AddToCartInput = z.infer<typeof schema>;

export function AddToCart() {
  const form = useForm<AddToCartInput>({
    resolver: zodResolver(schema),
    defaultValues: { size: "M", quantity: 1 },
  });

  const add = ({ size, quantity }: AddToCartInput) => {
    cart.add({
      id: `tee-${size}`,
      name: `Ocean Tee (${size})`,
      quantity,
      price: 28,
    });
    return `Added ${quantity} × Ocean Tee, size ${size}.`;
  };

  // An agent calls this.
  useWebTool({
    id: "add_to_cart",
    title: "Add to cart",
    description:
      "Add the Ocean Tee to the cart. On its product page only. " +
      "Next: get_cart, or go_to cart.",
    inputSchema: schema,

    // The size is a custom Select, with nothing to type into: put
    // the agent's values in the form before the cursor presses.
    fill: (values) => form.reset(values),

    execute: add,
  });

  // A person submits this.
  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(add)}
        className="flex items-end gap-2"
      >
        <FormField
          control={form.control}
          name="size"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Size</FormLabel>
              <Select
                value={field.value}
                onValueChange={(size) => size && field.onChange(size)}
              >
                <FormControl>
                  <SelectTrigger className="w-24">
                    <SelectValue />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {SIZES.map((size) => (
                    <SelectItem key={size} value={size}>
                      {size}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="quantity"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Quantity</FormLabel>
              <FormControl>
                <Input
                  type="number"
                  min={1}
                  className="w-20"
                  {...field}
                  // A number input hands the form a string.
                  onChange={(e) =>
                    field.onChange(e.target.valueAsNumber)
                  }
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Add to cart</Button>
      </form>
    </Form>
  );
}
```

**Next.js**

```tsx title="components/add-to-cart.tsx"
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useWebTool } from "@webmcp-agents/react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { cart } from "@/lib/cart";

const SIZES = ["S", "M", "L"] as const;

// One schema checks the agent's call and the person's submit.
const schema = z.object({
  size: z.enum(SIZES).describe("The size: S, M or L"),
  quantity: z
    .number()
    .int()
    .min(1)
    .max(10)
    .describe("How many to add, e.g. 2"),
});

type AddToCartInput = z.infer<typeof schema>;

export function AddToCart() {
  const form = useForm<AddToCartInput>({
    resolver: zodResolver(schema),
    defaultValues: { size: "M", quantity: 1 },
  });

  const add = ({ size, quantity }: AddToCartInput) => {
    cart.add({
      id: `tee-${size}`,
      name: `Ocean Tee (${size})`,
      quantity,
      price: 28,
    });
    return `Added ${quantity} × Ocean Tee, size ${size}.`;
  };

  // An agent calls this.
  useWebTool({
    id: "add_to_cart",
    title: "Add to cart",
    description:
      "Add the Ocean Tee to the cart. On its product page only. " +
      "Next: get_cart, or go_to cart.",
    inputSchema: schema,

    // The size is a custom Select, with nothing to type into: put
    // the agent's values in the form before the cursor presses.
    fill: (values) => form.reset(values),

    execute: add,
  });

  // A person submits this.
  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(add)}
        className="flex items-end gap-2"
      >
        <FormField
          control={form.control}
          name="size"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Size</FormLabel>
              <Select
                value={field.value}
                onValueChange={(size) => size && field.onChange(size)}
              >
                <FormControl>
                  <SelectTrigger className="w-24">
                    <SelectValue />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {SIZES.map((size) => (
                    <SelectItem key={size} value={size}>
                      {size}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="quantity"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Quantity</FormLabel>
              <FormControl>
                <Input
                  type="number"
                  min={1}
                  className="w-20"
                  {...field}
                  // A number input hands the form a string.
                  onChange={(e) =>
                    field.onChange(e.target.valueAsNumber)
                  }
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Add to cart</Button>
      </form>
    </Form>
  );
}
```

**TanStack Start**

```tsx title="src/components/add-to-cart.tsx"
import { zodResolver } from "@hookform/resolvers/zod";
import { useWebTool } from "@webmcp-agents/react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { cart } from "@/lib/cart";

const SIZES = ["S", "M", "L"] as const;

// One schema checks the agent's call and the person's submit.
const schema = z.object({
  size: z.enum(SIZES).describe("The size: S, M or L"),
  quantity: z
    .number()
    .int()
    .min(1)
    .max(10)
    .describe("How many to add, e.g. 2"),
});

type AddToCartInput = z.infer<typeof schema>;

export function AddToCart() {
  const form = useForm<AddToCartInput>({
    resolver: zodResolver(schema),
    defaultValues: { size: "M", quantity: 1 },
  });

  const add = ({ size, quantity }: AddToCartInput) => {
    cart.add({
      id: `tee-${size}`,
      name: `Ocean Tee (${size})`,
      quantity,
      price: 28,
    });
    return `Added ${quantity} × Ocean Tee, size ${size}.`;
  };

  // An agent calls this.
  useWebTool({
    id: "add_to_cart",
    title: "Add to cart",
    description:
      "Add the Ocean Tee to the cart. On its product page only. " +
      "Next: get_cart, or go_to cart.",
    inputSchema: schema,

    // The size is a custom Select, with nothing to type into: put
    // the agent's values in the form before the cursor presses.
    fill: (values) => form.reset(values),

    execute: add,
  });

  // A person submits this.
  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(add)}
        className="flex items-end gap-2"
      >
        <FormField
          control={form.control}
          name="size"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Size</FormLabel>
              <Select
                value={field.value}
                onValueChange={(size) => size && field.onChange(size)}
              >
                <FormControl>
                  <SelectTrigger className="w-24">
                    <SelectValue />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {SIZES.map((size) => (
                    <SelectItem key={size} value={size}>
                      {size}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="quantity"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Quantity</FormLabel>
              <FormControl>
                <Input
                  type="number"
                  min={1}
                  className="w-20"
                  {...field}
                  // A number input hands the form a string.
                  onChange={(e) =>
                    field.onChange(e.target.valueAsNumber)
                  }
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Add to cart</Button>
      </form>
    </Form>
  );
}
```

**Astro**

```tsx title="src/components/add-to-cart.tsx"
import { zodResolver } from "@hookform/resolvers/zod";
import { useWebTool } from "@webmcp-agents/react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { cart } from "@/lib/cart";

const SIZES = ["S", "M", "L"] as const;

// One schema checks the agent's call and the person's submit.
const schema = z.object({
  size: z.enum(SIZES).describe("The size: S, M or L"),
  quantity: z
    .number()
    .int()
    .min(1)
    .max(10)
    .describe("How many to add, e.g. 2"),
});

type AddToCartInput = z.infer<typeof schema>;

export function AddToCart() {
  const form = useForm<AddToCartInput>({
    resolver: zodResolver(schema),
    defaultValues: { size: "M", quantity: 1 },
  });

  const add = ({ size, quantity }: AddToCartInput) => {
    cart.add({
      id: `tee-${size}`,
      name: `Ocean Tee (${size})`,
      quantity,
      price: 28,
    });
    return `Added ${quantity} × Ocean Tee, size ${size}.`;
  };

  // An agent calls this.
  useWebTool({
    id: "add_to_cart",
    title: "Add to cart",
    description:
      "Add the Ocean Tee to the cart. On its product page only. " +
      "Next: get_cart, or go_to cart.",
    inputSchema: schema,

    // The size is a custom Select, with nothing to type into: put
    // the agent's values in the form before the cursor presses.
    fill: (values) => form.reset(values),

    execute: add,
  });

  // A person submits this.
  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(add)}
        className="flex items-end gap-2"
      >
        <FormField
          control={form.control}
          name="size"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Size</FormLabel>
              <Select
                value={field.value}
                onValueChange={(size) => size && field.onChange(size)}
              >
                <FormControl>
                  <SelectTrigger className="w-24">
                    <SelectValue />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  {SIZES.map((size) => (
                    <SelectItem key={size} value={size}>
                      {size}
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={form.control}
          name="quantity"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Quantity</FormLabel>
              <FormControl>
                <Input
                  type="number"
                  min={1}
                  className="w-20"
                  {...field}
                  // A number input hands the form a string.
                  onChange={(e) =>
                    field.onChange(e.target.valueAsNumber)
                  }
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />

        <Button type="submit">Add to cart</Button>
      </form>
    </Form>
  );
}
```

An agent calls `add_to_cart({ size: "L", quantity: 2 })`:

1. **The schema checks the input.** Input that fails it never reaches the page: the agent gets the schema's messages
   back, and the cursor does not move.
2. **The cursor types what it can.** It goes to the button named "Add to cart" (the tool's `title`), then types 2 into
   the field named `quantity`. React Hook Form's `onChange` hears it.
3. **`fill` puts the rest on screen.** The Size is a shadcn `Select`, a custom widget with nothing to type into, so
   `form.reset(values)` shows L.
4. **The cursor presses the button, and `add` runs.** Its return value is the agent's answer.

A person's submit goes through `form.handleSubmit`, which validates and calls `add`. An agent's call reaches `add`
directly, after the tool's own schema: the action is written once and runs once.

## How the cursor fills a form

The cursor finds a parameter's field by the field's `name`, then by a label that matches the schema's `title` for it
or the parameter read as words (`firstName` is "first name"). It looks outward from the tool's button, so the closest
field wins. Then it writes the value the way the control expects:

| Control | What happens |
|---|---|
| Text, email, tel, search, textarea | Typed a character at a time |
| `number`, `date`, `range`, a native `<select>` | Written once |
| Checkbox, radio | Clicked to match the value (in a radio group, the one whose `value` is the parameter's) |
| A custom widget: shadcn `Select`, `Combobox`, a date picker | Visited and highlighted, never written: use `fill` |

`fill` runs after the cursor typed what it could and before it presses or parks. It may return a promise, which is
awaited. With the cursor off, with reduced motion, or after the person moved the mouse, the values the cursor did not
type are written in one step, and `fill` still runs.

A number input hands React Hook Form a string, and a `z.number()` schema rejects it: pass
`e.target.valueAsNumber` to `field.onChange`, as the example does.

## When the fields are not named like the parameters

A parameter with no field is skipped: nothing is typed, and `execute` still gets the value. When the field has another
name, say where it is with `fields`, by parameter:

```tsx
useWebTool({
  id: "search_flights",
  description: "Search one-way flights between two airports.",
  inputSchema: searchSchema,
  fields: {
    from: "Leaving from", // the field labelled "Leaving from"
    to: { role: "combobox", name: "Going to" }, // a locator
    cabin: false, // never type this one
  },
  execute: search,
});
```

A field elsewhere on the page is typed there. A tool with no control at all types nothing unless `fields` names a
field.

## Validation errors are the answer

Two layers, and both reach the agent as readable text with an [error code](/docs/concepts/errors):

- **The schema.** Input that fails it is `invalid_input`, with each bad field named, before anything is typed. The
  agent fixes its call and retries.
- **Your own rules**, inside the action: throw a `WebToolError` with a code, and put the same words on the page, so the
  person sees what the agent read.

```tsx
const add = ({ size, quantity }: AddToCartInput) => {
  if (!inStock(size, quantity)) {
    const message = `Only ${stockOf(size)} left in size ${size}.`;
    form.setError("root", { message });
    throw new WebToolError("invalid_input", message);
  }
  // …
};
```

Show `form.formState.errors.root?.message` under the form. A person's submit runs the same function, so a throw there
is a rejected `handleSubmit`; catch it in the submit handler if you would rather not see it in the console. A tool that
has to ask the person something ("which size?") calls [`waitingForUser`](/docs/reference/core/waiting-for-user) and answers
with `needs_user`.

:::note[A form that leaves its page]
Navigate inside the action, with your router's own `navigate`, after the work is done. `useWebTool` keeps the tool
registered until the agent has its answer, so nothing else is needed.
:::

## TanStack Form

The same hook works with any form library: the cursor writes a field the way a person does (the native setter, then
`input` and `change` events), so any controlled input hears it. Here it is with TanStack Form:

```package-install
npm install @tanstack/react-form
```

**Vite**

```tsx title="src/components/subscribe.tsx"
import { useForm } from "@tanstack/react-form";
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

const schema = z.object({
  email: z
    .email()
    .describe("The address to subscribe, e.g. ada@example.com"),
});

type SubscribeInput = z.infer<typeof schema>;

export function Subscribe() {
  const [done, setDone] = useState("");

  const subscribe = ({ email }: SubscribeInput) => {
    setDone(`Subscribed ${email}.`);
    return `Subscribed ${email}.`;
  };

  const form = useForm({
    defaultValues: { email: "" },
    validators: { onSubmit: schema },
    onSubmit: ({ value }) => subscribe(value),
  });

  // An agent calls this.
  useWebTool({
    id: "subscribe_newsletter",
    title: "Subscribe",
    description:
      "Subscribe an email address to the newsletter. Use when the " +
      "user asks for news by email.",
    inputSchema: schema,
    execute: subscribe,
  });

  // A person submits this.
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void form.handleSubmit();
      }}
      className="flex items-end gap-2"
    >
      <form.Field name="email">
        {(field) => (
          <div className="grid gap-2">
            <Label htmlFor={field.name}>Email</Label>
            <Input
              id={field.name}
              // TanStack Form sets no name: the cursor needs one.
              name={field.name}
              type="email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
          </div>
        )}
      </form.Field>

      <Button type="submit">Subscribe</Button>

      {done && <p>{done}</p>}
    </form>
  );
}
```

**Next.js**

```tsx title="components/subscribe.tsx"
"use client";

import { useForm } from "@tanstack/react-form";
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

const schema = z.object({
  email: z
    .email()
    .describe("The address to subscribe, e.g. ada@example.com"),
});

type SubscribeInput = z.infer<typeof schema>;

export function Subscribe() {
  const [done, setDone] = useState("");

  const subscribe = ({ email }: SubscribeInput) => {
    setDone(`Subscribed ${email}.`);
    return `Subscribed ${email}.`;
  };

  const form = useForm({
    defaultValues: { email: "" },
    validators: { onSubmit: schema },
    onSubmit: ({ value }) => subscribe(value),
  });

  // An agent calls this.
  useWebTool({
    id: "subscribe_newsletter",
    title: "Subscribe",
    description:
      "Subscribe an email address to the newsletter. Use when the " +
      "user asks for news by email.",
    inputSchema: schema,
    execute: subscribe,
  });

  // A person submits this.
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void form.handleSubmit();
      }}
      className="flex items-end gap-2"
    >
      <form.Field name="email">
        {(field) => (
          <div className="grid gap-2">
            <Label htmlFor={field.name}>Email</Label>
            <Input
              id={field.name}
              // TanStack Form sets no name: the cursor needs one.
              name={field.name}
              type="email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
          </div>
        )}
      </form.Field>

      <Button type="submit">Subscribe</Button>

      {done && <p>{done}</p>}
    </form>
  );
}
```

**TanStack Start**

```tsx title="src/components/subscribe.tsx"
import { useForm } from "@tanstack/react-form";
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

const schema = z.object({
  email: z
    .email()
    .describe("The address to subscribe, e.g. ada@example.com"),
});

type SubscribeInput = z.infer<typeof schema>;

export function Subscribe() {
  const [done, setDone] = useState("");

  const subscribe = ({ email }: SubscribeInput) => {
    setDone(`Subscribed ${email}.`);
    return `Subscribed ${email}.`;
  };

  const form = useForm({
    defaultValues: { email: "" },
    validators: { onSubmit: schema },
    onSubmit: ({ value }) => subscribe(value),
  });

  // An agent calls this.
  useWebTool({
    id: "subscribe_newsletter",
    title: "Subscribe",
    description:
      "Subscribe an email address to the newsletter. Use when the " +
      "user asks for news by email.",
    inputSchema: schema,
    execute: subscribe,
  });

  // A person submits this.
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void form.handleSubmit();
      }}
      className="flex items-end gap-2"
    >
      <form.Field name="email">
        {(field) => (
          <div className="grid gap-2">
            <Label htmlFor={field.name}>Email</Label>
            <Input
              id={field.name}
              // TanStack Form sets no name: the cursor needs one.
              name={field.name}
              type="email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
          </div>
        )}
      </form.Field>

      <Button type="submit">Subscribe</Button>

      {done && <p>{done}</p>}
    </form>
  );
}
```

**Astro**

```tsx title="src/components/subscribe.tsx"
import { useForm } from "@tanstack/react-form";
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

const schema = z.object({
  email: z
    .email()
    .describe("The address to subscribe, e.g. ada@example.com"),
});

type SubscribeInput = z.infer<typeof schema>;

export function Subscribe() {
  const [done, setDone] = useState("");

  const subscribe = ({ email }: SubscribeInput) => {
    setDone(`Subscribed ${email}.`);
    return `Subscribed ${email}.`;
  };

  const form = useForm({
    defaultValues: { email: "" },
    validators: { onSubmit: schema },
    onSubmit: ({ value }) => subscribe(value),
  });

  // An agent calls this.
  useWebTool({
    id: "subscribe_newsletter",
    title: "Subscribe",
    description:
      "Subscribe an email address to the newsletter. Use when the " +
      "user asks for news by email.",
    inputSchema: schema,
    execute: subscribe,
  });

  // A person submits this.
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        void form.handleSubmit();
      }}
      className="flex items-end gap-2"
    >
      <form.Field name="email">
        {(field) => (
          <div className="grid gap-2">
            <Label htmlFor={field.name}>Email</Label>
            <Input
              id={field.name}
              // TanStack Form sets no name: the cursor needs one.
              name={field.name}
              type="email"
              value={field.state.value}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
            />
          </div>
        )}
      </form.Field>

      <Button type="submit">Subscribe</Button>

      {done && <p>{done}</p>}
    </form>
  );
}
```

Give each input the field's name (`name={field.name}`): TanStack Form does not set it, and it is how the cursor finds
the field. For a custom widget, `fill` calls `form.setFieldValue(…)`. Skyline's traveler form
(`examples/flights/src/webmcp-agents/hooks/use-traveler-details.ts`) is a larger one, with a field per traveler.

## Try it

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

```js
await agent.invoke("add_to_cart", { size: "L", quantity: 2 });
```

The cursor types 2 into Quantity, the Size shows L, the cursor presses **Add to cart**, and the call answers "Added 2 ×
Ocean Tee, size L." Try `quantity: 0`: the cursor stays put, and the call rejects with `invalid_input`, naming the bad
field.

## Options you'll use most

| Option | What it is for |
|---|---|
| `inputSchema` | The parameters, as a zod (or any Standard Schema) object or JSON Schema. Validated before anything moves. |
| `fill` | Put values on screen yourself, for widgets the cursor cannot type into. |
| `fields` | Where each parameter is typed, when the field is not named like it: a label, a locator, or `false`. |
| `target` | Which button the cursor presses, when the `title` does not name it. |
| `execute` | The action, shared with the form's submit. Its return value is the answer. |

Every option: [`useWebTool`](/docs/reference/react/use-web-tool) and [`defineWebTool`](/docs/reference/core/define-web-tool).
