Skip to navigation

Custom verbs

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.

Defining a verb

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

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:

{
"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.

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.

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.