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

# Constraints

A Noun is a name, its fields and its Verbs. Field keys are camelCase, and a Verb moves a record between named states. Each rule exists because some surface depends on it, and each one names that surface.

Inside these rules, your agent writes a Noun file right the first time, and each improvement to a generated surface reaches your product the day it ships.

## The shape of a Noun

This is Leaseclock's Lease, in full:

```yaml title="leaseclock.com/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 }
---
```

The file holds the three keys that make up the Noun: `name`, `fields` and `verbs`. A Noun that people only keep, such as Unit, leaves out `verbs`. The folder says what kind of definition a file is, and its path is its address, so `Noun/Lease.mdx` is the Noun at `leaseclock.com/Noun/Lease` and never repeats either.

## What one name becomes

Each name in the file appears on every surface, spelled the same way or derived from it by one fixed rule:

| Surface | `Lease` | `endsOn` | `renew` |
| --- | --- | --- | --- |
| App | "Leases" in the sidebar | An "Ends on" column and a date picker | A Renew button on each active lease |
| API | `leaseclock.com/api/leases` | `"endsOn": "2027-11-30"` | `POST /api/leases/4b/renew` |
| CLI | `npx leaseclock show leases` | `--endsOn 2027-11-30` | `npx leaseclock do Lease.renew 4b` |
| SDK | `$.Lease` | `lease.endsOn` | `$.Lease.renew('4b', { endsOn })` |
| MCP | `search` returns `leases/4b` | `"input": { "endsOn": … }` | `do` with `"verb": "Lease.renew"` |
| Docs | `leaseclock.com/docs/lease` | The Ends on row of the Lease page | `leaseclock.com/docs/lease#renew` |

Rename Lease to Tenancy and every cell follows: `/api/tenancies`, `$.Tenancy`, "Tenancies" in the sidebar.

## The rules

### Keys a Noun takes

A Noun file takes `name`, `fields` and `verbs`, and no other key. Every surface reads those three keys, so a key outside them would reach one surface and miss the rest.

```text
422  Unit.mdx line 3: a Noun takes name, fields and verbs. Rename "properties" to "fields".
```

### Noun names

A Noun's name starts with a capital letter and holds letters and digits, up to 64 characters: `Lease`, `Unit`, `RentalAgreement`. The name is the URL segment (`leases`), the screen title ("Leases"), the SDK namespace (`$.Lease`), the CLI target (`Lease.renew`) and the docs page (`/docs/lease`), and each is derived from that one spelling.

```text
422  Lease.mdx line 2: name "lease" must start upper-case. Rename it to "Lease".
```

### Field keys and Verb names

Field keys and Verb names are camelCase: `endsOn`, `renew`, `markPaid`. Each one is a JSON key in the API, a property in the SDK, a flag in the CLI and an argument your customers' agents send. The App writes its label from the same key, so `endsOn` heads a column "Ends on" and `markPaid` labels a button "Mark paid".

```text
422  Lease.mdx line 6: field "EndsOn" must start lower-case. Rename it to "endsOn".
```

### Field types

Every field has a `type` from the set on [Nouns](https://saas.dev/docs/nouns.md): `text`, `email`, `date`, `money`, `link`, `choice` and the rest. The type decides the list column, the form input, the check on every write, the JSON schema and the SDK type, so `endsOn` gets a date picker in the App and an ISO date such as `2027-11-30` in the API and the SDK.

```text
422  Unit.mdx line 6: field "bedrooms" has no type "integer". Use "number".
```

### Links

A `link` field names its `target`, and the target is a Noun in the same product. Lease's `unit` points at Unit, so the form offers a Unit picker, the list shows the unit as a link, and the API rejects a lease for a unit that does not exist.

```text
422  Lease.mdx line 4: field "unit" links to "Units", and no Noun has that name. Use "Unit".
```

### Verbs and states

A Verb says what it does in `does`, and that sentence is the docs line, the CLI help, the SDK hover text and the description your customers' agents read. A Verb that changes state names it: `from` lists the states a record must be in, and `to` names the state it lands in. Both are options of the Noun's `status` field.

Only Verbs change `status`. The Lease form has no status box, and every change of state has a button, an endpoint, a history entry and an undo. The App disables Renew on an ended lease and shows why, and every other surface answers the same way:

```bash title="Terminal"
npx leaseclock do Lease.renew 5a --endsOn 2027-09-30
# 409  Lease 5A is ended. Renew runs only on an active lease.
```

A state that `status` does not list is an error on push:

```text
422  Lease.mdx line 13: verb "end" moves to "closed", which is not a status. Use "ended", or add "closed" to status.
```

### Prices

What your customers pay you lives in an Offer, as an `amount`, a `currency` and a `unit`:

```yaml title="leaseclock.com/Offer/pro.mdx"
price: { amount: 29, currency: USD, unit: month }
limits: { Unit: 200 }
```

The pricing page, the Stripe price, Checkout and the upgrade prompt in the App all read that one line, so they always agree. `limits` sets how many Units each customer keeps, and the API holds every customer to it. A `money` field such as a lease's rent is a record's data, and it charges nobody.

```text
422  pro.mdx line 3: price has no key "interval". Use "unit: month".
```

### Logic

Code you write runs as a Verb's function, in `fn`. Renew's function sets the new end date, and the Verb around it gives that code its button, its endpoint, its CLI command, its tool for agents, a preview before it runs and an undo after. A screen of your own reads and writes through the same Verbs, so permissions and undo hold there too. [Defaults and overrides](https://saas.dev/docs/overrides.md) shows how to replace a screen.

## When a file breaks a rule

[`npx saas.dev push`](https://saas.dev/docs/saas-dev-cli.md) checks every file before it saves anything. When lines break a rule, the push saves nothing and returns every one at once, each with the file, the line and the fix:

```console title="Terminal"
$ npx saas.dev push
422  Lease.mdx line 6: field "EndsOn" must start lower-case. Rename it to "endsOn".
422  pro.mdx line 4: limits names "Units", and no Noun has that name. Use "Unit".

$ npx saas.dev push --fix
Pushed version 2 · https://my-product.test.saas.dev
```

`--fix` applies each error's edit, then previews. With `--json`, every error carries the same edit as `{ file, line, from, to }`, so your agent can apply it itself. Every code has an entry in [Errors](https://saas.dev/docs/errors.md) with its cause and fix.

## What fits

SaaS.Dev fits products made of records and the things people do to them. Leaseclock is one: it keeps Units and Leases, and property managers renew and end leases. Trackers, CRMs, booking and scheduling tools, client portals and back offices take the same shape.

Inside a product that fits, the parts that are yours have a place. A custom screen replaces one view and keeps your theme and permissions, and custom logic is a Verb's function. The fewer defaults you replace, the more improvements reach your product for free.

Games, document and design editors, and video or audio tools need a different platform. Their core is a canvas, a game loop or a media stream, and no list, form or endpoint describes it.
