Verb

Docs

Writing verb.md

verb.md is a plain markdown file at your repository root with one section per action the assistant may take. It is the complete list: a tool that is not in this file does not exist as far as the assistant is concerned, however the question is phrased.

Have your coding agent write it

This is the fastest path by a distance, and it is not a shortcut. Your dashboard's install step has a copy-paste prompt. Hand it to whichever coding agent you already use in that repository.

bash
npx --yes @askverb/skill@latest install --dir <your-repo>/.claude/skills
# then, in that repo: "add Verb to this app"

It installs a skill that teaches the format, then reads your actual routes rather than guessing at endpoints. An agent that can see your router writes better tool descriptions than you will from memory, because it uses the real parameter names.

What a tool looks like

markdown
## cancel_order

Cancels an order that has not shipped. Call this when the user asks to
cancel, stop or undo an order they placed.

- classification: destructive
- run: browser
- confirm: Cancel order {order_id}

### Arguments

- `order_id` (string, required) The order to cancel, for example "1041"

```js
return fetch(`/api/orders/${args.order_id}/cancel`, { method: "POST" })
  .then((r) => r.json());
```

The description is the part worth spending time on. It is what the model reads when deciding whether this is the right tool, so say when to call it rather than what it does internally. "Call this when the user asks to cancel, stop or undo an order" is worth more than a paragraph about your order state machine.

Arguments are deliberately flat and scalar: string, number, integer, boolean, file. There are no nested objects or arrays, and that is a choice rather than a gap. A model fills flat arguments correctly far more often, and your adapter is a better place to build the shape your API actually wants.

Type them as precisely as your app does, because they become a form. When a value is missing, or the model had no source for it, the confirmation card asks the user for it, and the inputs are built from these types. A seat count typed integer, min 1, max 8 is a number box that refuses letters; typed string it accepts "two". Everything goes in the parentheses:

markdown
- `seats` (integer, required, min 1, max 8) How many seats
- `contact_email` (string, email, required) Where the tickets go
- `seat_type` (string, required, one of: standard, premium, accessible) Which section
- `booking_ref` (string, required, matches ^[A-Z]{2}\d{4}$) Like AB1234
- `id_photo` (file, image, optional, max 5mb) Photo ID

Formats are email, url, date and tel. one of: is radio buttons up to four options and a select from five. A file takes image, pdf, csv or a mime type, and a size in kb, mb or gb: that size is the only limit, so always give one. The file goes from the browser straight to your endpoint, never through Verb. Optional arguments are not asked for, except files, so mark required whatever the action cannot run without.

So do not mirror your endpoint's input schema. Take status as a string and let the adapter nest it into filters: { statuses: [...] }. Take ISO strings and let the adapter construct the dates. Take one guest per call and let the adapter build the array. If a capability genuinely cannot be expressed this way, leave it out: the assistant declines cleanly, which is much better than it improvising a wrong shape and your API accepting it.

Classification is the safety model

Every tool is one of three, and this is what decides how it behaves at runtime:

  • read runs on its own. Nothing is confirmed, because nothing changes.
  • write stops and shows the user a confirmation card with the real arguments before anything runs.
  • destructive is the same gate, held to a higher bar in the safety review, and worth leaving switched off until somebody has asked for it twice.

The confirm: line is the wording on that card. Write it with the argument in it, Cancel order {order_id}, because a card that says "confirm this action?" teaches people to click through it and the entire value of the gate is that somebody read it.

Where a tool runs

run: browser is a same-origin fetch. Verb makes it, and there is nothing to wire up.

run: adapter is anything else: Supabase, Firebase, tRPC, an API on another host. It goes through your own client, because Verb never holds your credentials. That is its own page

Describe them well before you worry about how many

Start with one real workflow, then cover the rest of what your users actually ask for. What costs you accuracy is a vague description, not a long list: every entry that overlaps another is a chance for the model to pick a near-miss, and a near-miss is worse than a refusal because it looks like an answer.

A 28 tool integration was tested against near-miss pairs, destructive-adjacent phrasing and multi-step chains, and picked correctly throughout. So do not stop early to keep the count down. Cut the tools that mirror your route table rather than the ones somebody would ask for by name.

Pick the workflow your users ask for most, which for most products is creation, assignment and access management, and add more once you can see people asking.

A destructive action that touches a Supabase or Firebase table directly, rather than going through an RPC, is refused outright by the safety review. There is no override. Put it behind a function where your own rules can run.

Importing it

Paste the file into the dashboard. Every tool is parsed, reviewed and imported as a draft or blocked, and read-only tools switch themselves on. Nothing that writes activates without you choosing it.

Re-import any time. Your own decisions about what is switched on survive the re-import; a tool the review has blocked stays blocked.

Stuck on something this does not cover? Write to us and you reach the person who built it.