---
url: https://saas.dev/docs/verbs
md: https://saas.dev/docs/verbs.md
---

# Verbs

A Verb is something people or agents do to a Noun. Lease has two: `renew` moves the end date forward, and `end` closes the lease. A Verb declares what it does, the input it takes and the states it moves between, and each Verb becomes a button, an endpoint, a command and a tool.

```yaml title="Noun/Lease.mdx"
---
name: Lease
fields:
  unit: { type: link, target: Unit, required: true }
  renter: { type: email, required: true }
  endsOn: { type: date, required: true, indexed: true }
  status: { type: choice, options: [{ key: active }, { key: ended }] }
verbs:
  renew:
    does: Move the end date forward
    input: { endsOn: { says: the new end date, type: date, required: true } }
    from: [active]
  end: { does: End the lease, from: [active], to: ended }
---
```

## Renew a lease

Northgate Rentals runs its 24 units on [Leaseclock](https://leaseclock.com). Dana Whitfield's lease on Unit 4B ends on November 30, 2026, and Priya Shah, the leasing coordinator, renews it through November 30, 2027. She can do that from any surface. Each one runs the same `renew`, takes the same input, checks the same state and returns the same lease.

**App**

At [leaseclock.com/app/leases](https://leaseclock.com/app/leases), every active lease has a **Renew** button. Priya opens it on 4B, and the popover asks for one thing: **The new end date**. It shows Dana's current end date beside the field, so Priya sees the change before she presses **Renew**. The lease page then records who renewed it and when, with **Undo** beside the entry.

**API**

```bash title="Terminal"
curl -X POST https://leaseclock.com/api/leases/4b/renew \
  -H "Authorization: Bearer $LEASECLOCK_TOKEN" \
  -H "Idempotency-Key: renew-4b-2027-11-30" \
  -H "Content-Type: application/json" \
  -d '{ "endsOn": "2027-11-30" }'
```

```json title="200 OK"
{
  "$id": "https://leaseclock.com/api/leases/4b",
  "$type": "Lease",
  "unit": "4b",
  "renter": "dana.whitfield@example.com",
  "endsOn": "2027-11-30",
  "status": "active",
  "renewedBy": "priya@northgaterentals.com",
  "undo": "https://leaseclock.com/api/leases/4b/undo"
}
```

Every Verb POST carries an `Idempotency-Key`, so a retry never renews twice. The [API reference](https://saas.dev/docs/api.md) lists every Verb's endpoint.

**CLI**

```bash title="Terminal"
$ npx leaseclock do Lease.renew 4b --endsOn 2027-11-30
Preview  Lease 4B  endsOn 2026-11-30 → 2027-11-30
Renew it? y
✓ Renewed lease 4B. It now ends 2027-11-30.
  Undo: npx leaseclock undo Lease.renew 4b
```

Add `--yes` to skip the question in a script. The [CLI](https://saas.dev/docs/cli.md) runs every Verb with `do`.

**SDK**

```ts title="renew.ts"
import { Leaseclock } from 'leaseclock'

const $ = new Leaseclock({ token: process.env.LEASECLOCK_TOKEN })

const lease = await $.Lease.renew('4b', { endsOn: '2027-11-30' })

lease.endsOn   // '2027-11-30'
lease.status   // 'active'
```

`renew` takes the lease's id and an input typed from the definition, so your editor flags a missing `endsOn` before the code runs. See the [SDK](https://saas.dev/docs/sdk.md).

**MCP**

Priya asks her agent: *"Renew Dana Whitfield's lease in 4B through November 30, 2027."* The agent finds the lease with `search`, then calls `do`:

```json title="do"
{
  "verb": "Lease.renew",
  "id": "4b",
  "input": { "endsOn": "2027-11-30" }
}
```

`do` runs any Verb, so a new Verb reaches every connected agent without a new tool. See [MCP](https://saas.dev/docs/mcp.md).

## Name

The key under `verbs:` names the Verb. Write it in camelCase, starting lower-case, as something a person does: `renew`, `end`, `markPaid`. The name sets every place the Verb appears:

| Surface | `renew` on Lease becomes |
| --- | --- |
| App | A **Renew** button on each lease (`markPaid` becomes **Mark paid**) |
| API | `POST /api/leases/{id}/renew` |
| CLI | `npx leaseclock do Lease.renew <id>` |
| SDK | `$.Lease.renew(id, input)` |
| MCP | `"verb": "Lease.renew"` in the `do` tool |
| Docs | [leaseclock.com/docs/lease#renew](https://leaseclock.com/docs/lease#renew) |

Push a new Verb and every surface has it on the next request: the button, the endpoint, the command, the method, the docs section and a changelog entry.

## What it does

`does` is one sentence, written as an instruction to the person about to run the Verb. Leaseclock shows it as the hint on the Renew button, the summary at the top of the Verb's docs section, the description in the OpenAPI file and the help text in the CLI. Agents read it too, which is how they pick the right Verb.

```text title="Terminal"
$ npx leaseclock do Lease.renew --help
Move the end date forward

Usage: npx leaseclock do Lease.renew <id> --endsOn <date>

  --endsOn  date  required  The new end date

Runs from: active
```

## Input

`input` lists what the Verb needs from whoever runs it. Each input is a field, with the same types and options as a Noun's [fields](https://saas.dev/docs/nouns.md), plus `says`, the label a person reads.

| Line | What it generates |
| --- | --- |
| `endsOn:` | The input's key in the request body, the SDK type and the tool's input schema, and the `--endsOn` flag |
| `says: the new end date` | The popover's label, **The new end date**, and the description in the docs and the CLI help |
| `type: date` | A date picker in the popover, and a check that rejects anything that is not a date |
| `required: true` | A required mark in the popover, and a 422 that names `endsOn` when it is missing |

A Verb with no `input` runs on one click. `end` takes none, so its button asks only for confirmation.

## States

`from` lists the states a Verb runs from, and `to` names the state it leaves the record in. Both name options of the Noun's `status` field. `end` runs from `active` and moves the lease to `ended`. `renew` has no `to`, so the lease stays `active` with its new end date. A Verb with no `from` runs in any state.

On a lease in any other state, the Verb does not run, and every surface says why in the same words. Hannah Cho's lease on 5A ended on September 30, 2026, so its Renew and End buttons are disabled with the reason beside them, and the API answers 409:

```bash title="Terminal"
$ npx leaseclock do Lease.end 5a
✗ Lease 5A is ended. end runs from active.
```

The states also drive the list: each `status` option gets a tab, and a lease's badge changes the moment a Verb moves it.

## Logic

A Verb with no `fn` writes its input onto the record. `renew` declares `endsOn` as its input, so it sets `endsOn` and touches nothing else, and `end`, which takes no input, only moves the lease to `ended`. Most Verbs need nothing more.

When a Verb needs logic of its own, export a function with the Verb's name after the front matter of the Noun's file. It receives the record and the input, and returns the fields to change. Throw to stop the Verb, and the message reaches whoever ran it, in the popover, the API error, the CLI or the agent's tool result.

```mdx title="Noun/Lease.mdx"
export const renew = async (lease, { endsOn }) => {
  if (endsOn <= lease.endsOn) throw new Error('endsOn must be later')
  return { endsOn }
}
```

The function runs inside SaaS.Dev with the same permissions, preview, undo and history as every other Verb. You maintain that one function. [Defaults and overrides](https://saas.dev/docs/overrides.md) covers the rest of what you can replace.

## Preview, undo and history

Every Verb comes with these, from the first push:

- **Preview.** Each surface shows the change before it commits. The popover shows the current and new values, the CLI asks before it runs, and on the API a GET on the Verb's URL returns the preview and changes nothing.
- **Undo.** Each run saves a version of the record. The API returns an `undo` link, the CLI prints the undo command, and the lease page has an Undo button.
- **History.** The lease page lists every run: which Verb, who ran it, when, and the values before and after.
- **Permissions.** Roles decide who may run each Verb, and every surface checks the same rule. A customer's team sees only its own workspace's leases.
- **Retries.** A POST with an `Idempotency-Key` that was already used returns the first result and runs nothing again.

## Rules

A Verb's name is camelCase and starts lower-case. Every state in `from` and `to` is an option of the Noun's `status` field. Input keys follow the same rules as field keys. When a file breaks a rule, the push returns the line and the fix, so you or your agent correct it in one edit:

```text
422  Lease.mdx line 12: renew runs from "Active", which is not a status of Lease. Use "active".
```

[Constraints](https://saas.dev/docs/constraints.md) explains why each rule exists, and [Errors](https://saas.dev/docs/errors.md) lists every code with its fix.
