> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withboundary.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rules

> Encode domain correctness as named, deterministic checks

Schemas say what data looks like. Rules say whether it can be trusted.

A rule is a named check with a description, a function that returns `true` or a failure message, and an optional list of fields it touches. Rules run after schema validation; if any rule fails, Boundary turns the failure into a repair message and asks the model to try again.

## The `Rule` type

```typescript theme={null}
interface Rule<T> {
  name: string;                         // stable machine key, unique per contract
  description?: string;                 // positive statement of the invariant
  check: (data: T) => boolean | string; // return true to pass, false/string to fail
  message?: string;                     // fallback failure text when check returns false
  fields?: string[];                    // fields this rule reads (auto-inferred when omitted)
}
```

| Field         | Required | Notes                                                                                                                                                                                           |
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | yes      | Stable machine key. ≤128 chars, unique within the contract. Joins to per-rule failure counts on the dashboard, so don't rename it casually. Use `snake_case`.                                   |
| `description` | no       | Human-readable invariant in the positive ("Hot leads must have a score ≥ 70"), ≤1000 chars. Shown in dashboards, diffs, and code review.                                                        |
| `check`       | yes      | Synchronous, deterministic, cheap. Returns `true` to pass, `false` to fail with `message`, or a string to fail with that string as the failure message (overrides `message`).                   |
| `message`     | no       | Static fallback when `check` returns plain `false`. If `check` returns a string, that string wins.                                                                                              |
| `fields`      | no       | Field names this rule reads. ≤64 entries, each ≤128 chars. **Auto-inferred from the check function source when omitted** — supply explicitly if you minify or your check delegates to a helper. |

## A canonical rule

```typescript theme={null}
import { defineContract } from "@withboundary/contract";

defineContract({
  name: "lead-scoring",
  schema,
  rules: [
    {
      name: "hot_requires_high_score",
      description: "Hot leads must have a score of at least 70",
      fields: ["tier", "score"],
      check: (lead) =>
        lead.tier !== "hot" || lead.score >= 70
          || `tier is "hot" but score is ${lead.score} (minimum 70 for hot)`,
    },
  ],
});
```

A few things this snippet bakes in:

* The `name` is `snake_case` and reads like a machine key. The dashboard joins on it.
* The `description` is a *positive* statement of the invariant, not the error text.
* The `check` short-circuits the not-applicable case (`lead.tier !== "hot"`), then asserts the invariant (`lead.score >= 70`). When neither holds, it returns a *dynamic* failure string — context like the actual score and the threshold makes the model's repair faster.
* `fields` identifies the output fields the rule reads. Boundary can infer simple cases, but explicit fields make docs, logs, and dashboard filters easier to read.

## What `check` may return

| Return value | Outcome                                                                                                    |
| ------------ | ---------------------------------------------------------------------------------------------------------- |
| `true`       | Rule passes for this attempt.                                                                              |
| `false`      | Rule fails; uses `rule.message` as the failure text (or "rule \<name> failed" if `message` is also unset). |
| `string`     | Rule fails; the returned string becomes the failure text and is sent to the model verbatim.                |

Returning a tailored string is almost always better than returning `false` with a static `message` — the closer the failure text is to "here's what's wrong, here's what to do," the better the repair on the next attempt.

## Sync, deterministic, cheap

<AccordionGroup>
  <Accordion title="Synchronous">
    `check` is not async. Rules run inline during validation. If you need an async check (database lookup, external API), do it before or after the contract — never inside.
  </Accordion>

  <Accordion title="Deterministic">
    Same input, same output. No randomness, no clocks, no external state. Determinism is what makes rules unit-testable and what lets the dashboard report stable per-rule failure counts.
  </Accordion>

  <Accordion title="Cheap">
    Rules run on every attempt — up to `maxAttempts` times per contract call. Keep them in-memory. No DB queries, no fetch, no heavy parsing.
  </Accordion>
</AccordionGroup>

## Domain examples

### Lead scoring

```typescript theme={null}
rules: [
  {
    name: "hot_requires_high_score",
    description: "Hot leads must have a score of at least 70",
    fields: ["tier", "score"],
    check: (lead) =>
      lead.tier !== "hot" || lead.score >= 70
        || `tier is "hot" but score is ${lead.score} (minimum 70 for hot)`,
  },
  {
    name: "reason_required",
    description: "Every scoring decision must have a non-empty reason",
    fields: ["reason"],
    check: (lead) => lead.reason.trim().length > 0 || "reason cannot be empty",
  },
]
```

### Finance

```typescript theme={null}
rules: [
  {
    name: "invoice_math_consistent",
    description: "Subtotal plus tax must equal total within a cent",
    fields: ["subtotal", "tax", "total"],
    check: (invoice) =>
      Math.abs(invoice.subtotal + invoice.tax - invoice.total) < 0.01
        || `subtotal + tax != total (${invoice.subtotal} + ${invoice.tax} = ${invoice.subtotal + invoice.tax}, got ${invoice.total})`,
  },
  {
    name: "currency_matches_account",
    description: "Invoice currency must match the account currency",
    fields: ["currency", "accountCurrency"],
    check: (invoice) =>
      invoice.currency === invoice.accountCurrency
        || `currency mismatch: got ${invoice.currency}, expected ${invoice.accountCurrency}`,
  },
]
```

### Support ops

```typescript theme={null}
rules: [
  {
    name: "high_priority_requires_reason",
    description: "High-priority tickets must include an escalation reason",
    fields: ["priority", "reason"],
    check: (ticket) =>
      ticket.priority !== "high" || ticket.reason.trim().length > 0
        || "high priority requires escalation reason",
  },
]
```

### Agents

```typescript theme={null}
rules: [
  {
    name: "cant_close_in_review",
    description: "Cannot close a ticket while it's in review",
    fields: ["action", "status"],
    check: (action) =>
      !(action.action === "close_ticket" && action.status === "needs_review")
        || "cannot close a ticket with status needs_review",
  },
]
```

### Compliance

```typescript theme={null}
rules: [
  {
    name: "us_requires_ssn",
    description: "US users must provide an SSN",
    fields: ["country", "ssn"],
    check: (account) =>
      account.country !== "US" || account.ssn !== null
        || "US users require SSN",
  },
  {
    name: "approve_requires_identity",
    description: "Auto-approval requires verified identity",
    fields: ["approved", "identityVerified"],
    check: (account) =>
      !account.approved || account.identityVerified
        || "cannot auto-approve without verified identity",
  },
]
```

## String messages drive repair

The string a failed rule returns becomes part of the repair prompt sent to the model. Specific, contextual messages produce specific, contextual repairs.

<Tabs>
  <Tab title="Bad">
    ```typescript theme={null}
    {
      name: "score_valid",
      check: (lead) => lead.score > 70 || "invalid",
    }
    ```

    The model sees: *"invalid"*. Not enough to fix anything.
  </Tab>

  <Tab title="Good">
    ```typescript theme={null}
    {
      name: "hot_requires_high_score",
      description: "Hot leads must have a score of at least 70",
      fields: ["tier", "score"],
      check: (lead) =>
        lead.tier !== "hot" || lead.score >= 70
          || `tier is "hot" but score is ${lead.score} (minimum 70 for hot)`,
    }
    ```

    The model sees the violation, the actual value, and the threshold. It knows what to fix and why.
  </Tab>
</Tabs>

## Multiple rules

Rules are evaluated in order. **All failing rules are collected** — Boundary does not short-circuit on the first failure. Every violation goes back to the model in a single repair message.

```typescript theme={null}
rules: [
  {
    name: "hot_requires_high_score",
    fields: ["tier", "score"],
    check: (lead) => lead.tier !== "hot" || lead.score >= 70 || `score ${lead.score} too low for hot`,
  },
  {
    name: "reason_required",
    fields: ["reason"],
    check: (lead) => lead.reason.trim().length > 0 || "reason cannot be empty",
  },
  {
    name: "score_in_range",
    check: (lead) => (lead.score >= 0 && lead.score <= 100) || "score must be between 0 and 100",
  },
]
// If all three fail, the model gets all three failures at once.
```

## Validation at construction time

`defineContract` validates rules when you build the contract — not lazily on the first run. You catch typos, duplicate names, and over-long descriptions at startup, not in production:

* `name` is required, ≤128 chars, unique within the contract.
* `description` is ≤1000 chars when provided.
* `fields` is ≤64 entries, each ≤128 chars when provided.
* `check` must be a function.

Anything that fails these checks throws a `TypeError` at `defineContract` time. Test it once with `pnpm tsc --noEmit` or your test runner; it'll never bite you in prod.

## Next steps

<CardGroup cols={2}>
  <Card title="The Repair Loop" icon="rotate" href="/concepts/repair-loop">
    How violations become targeted fixes
  </Card>

  <Card title="Results" icon="check" href="/concepts/results">
    What you get back from a contract call
  </Card>
</CardGroup>

## Observability

Named rules are useful locally and in production. Use [local development logging](/guides/local-development) to see exact rule failures while building, then add [production observability](/guides/production-observability) to track top failing rules across traffic.
