> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# Custom verbs

> Name the actions your business actually cares about — phi.export, trade.settle, patient.discharge — and put policy on them.

Writ ships with a fixed set of built-in verbs (`refund`, `admit`,
`prescribe`, `provision`, `verify`, `verify_human`, …). They cover common
ground, but they are not your domain language. **Custom verbs** let you name
the consequential actions your agents actually take — `phi.export`,
`trade.settle`, `patient.discharge` — and put Writ policy on them, with the
same checks, grants, receipts, and audit chain as everything else.

Verbs attach to principals: a check always runs as a principal's key, and
every receipt snapshots the principal. See [Principals](/principals).

## Defining a verb

`POST /v1/verbs` with a `name` and a `description`:

```bash
curl -X POST https://api.withwrit.com/v1/verbs \
  -H "Authorization: Bearer $WRIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "phi.export",
       "description": "Agent exported PHI outside the enclave"}'
```

Names are lowercase `domain.action` style: 1–64 characters of letters,
digits, dots, and underscores, with **at least one dot** — bare words are
reserved for built-in verbs. A name may not collide with a built-in verb
(case-insensitive) or an existing custom verb. A verb is usable in policies
the moment it is created. The response is the verb record:

```json
{
  "name": "phi.export",
  "builtin": false,
  "status": "live",
  "description": "Agent exported PHI outside the enclave",
  "aliasOf": null,
  "createdAt": 1759000000,
  "deprecatedAt": null
}
```

Tenants are capped at 200 verbs (including deprecated ones).

## Setting policy on custom verbs

Custom verbs are ordinary verbs everywhere else: `PUT /v1/policy` (and the
per-environment overrides) accepts them the moment they exist, and
`GET /v1/policy` lists the live ones in its `customVerbs` field. Unknown
verbs still fail closed to `require_grant` — defining the verb is what
promotes it from "deny by default" to a first-class policy target.

```bash
curl -X PUT https://api.withwrit.com/v1/policy \
  -H "Authorization: Bearer $WRIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phi.export": "step_up"}'
```

## Rename is safe

Language changes. `PATCH /v1/verbs/{name}` renames a live verb (and/or
updates its description), and the rename cannot silently drop protection:

* The old name becomes a **deprecated alias** — checks that name it still
  resolve, and evaluate against the *current* verb's policy. Old code and
  old receipts keep their meaning.
* Policies that used the old name are **migrated to the new name** — in the
  tenant default and in every environment override — so a rename can never
  silently drop a `deny`. Each touched policy's version bumps by one.
* The old name is **rejected in new policies**, with the offending verbs
  listed in the 400 response.

The receipt for a check that resolved an alias carries `resolvedVerb`: the
verb whose policy actually decided. `verb` always echoes exactly what the
caller sent. See the [data dictionary](/data-dictionary#receipt).

## Retire, don't delete

`DELETE /v1/verbs/{name}` retires a verb: it becomes `deprecated`, checks
that name it still resolve (its own policy still applies), and the name can
never be reused or re-created — so old receipts stay interpretable. Retired
verbs are rejected in new policies. Only live verbs can be retired.

Fetch the full vocabulary — built-ins plus custom, each with its status and
rename history — with `GET /v1/verbs`. The scanner reads this manifest to
map its findings onto your vocabulary; see
[Scanner](/scanner#preferring-your-vocabulary).