> ## 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.

# Contracts

> Schema + rules — define what correct means

A contract is the core abstraction in Boundary. It pairs a **schema** (what the data looks like) with **rules** (whether the data can be trusted). Together, they form the acceptance boundary between your LLM and your application.

## Defining a contract

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

const leadContract = defineContract({
  name: "lead-scoring",
  schema: z.object({
    tier: z.enum(["hot", "warm", "cold"]),
    score: z.number().min(0).max(100),
    reason: z.string(),
  }),
  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",
    },
  ],
});
```

`defineContract` returns a `DefinedContract<T>` — a reusable object you call `.accept()` on whenever you want to run an LLM call through this contract.

## Using a contract

The `.accept()` method takes your LLM call as a function. You own the LLM call. Boundary owns the validation.

```typescript theme={null}
const result = await leadContract.accept(async (attempt) => {
  const response = await callYourLLM({
    messages: [
      {
        role: "user",
        content: [
          "Score this lead as JSON.",
          attempt.instructions,
          leadSummary,
        ].join("\n\n"),
      },
      ...attempt.repairs,
    ],
  });

  return response.text;
});
```

The `attempt` object gives you everything you need:

| Field          | Type                       | Description                                                                                                                                                                             |
| -------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `attempt`      | `number`                   | Current attempt number (1-indexed)                                                                                                                                                      |
| `maxAttempts`  | `number`                   | Maximum retries allowed                                                                                                                                                                 |
| `instructions` | `string`                   | Auto-generated prompt from your schema                                                                                                                                                  |
| `repairs`      | `Array<{ role, content }>` | Repair messages from prior failures — empty on the first attempt, always `role: "user"` on retries. See [the `attempt.repairs` field](/concepts/repair-loop#the-attempt-repairs-field). |

## The `RunFn`

Your function must match this signature:

```typescript theme={null}
type RunFn = (attempt: ContractAttempt) => Promise<string | null>
```

Return the raw LLM response as a string. Boundary handles parsing, validation, and repair. Return `null` if the LLM returned nothing.

<Note>
  The `RunFn` must return a **string**, not a parsed object. Extract the text content from your provider's response object before returning.
</Note>

## Your existing Zod schemas work

Boundary uses Zod for schema validation. If you already have Zod schemas in your codebase, pass them directly — no migration, no rewrite, no Boundary-specific DSL.

```typescript theme={null}
// your existing schema
const invoiceSchema = z.object({
  subtotal: z.number(),
  tax: z.number(),
  total: z.number(),
});

// just add rules
const invoiceContract = defineContract({
  name: "invoice-extraction",
  schema: invoiceSchema,
  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})`,
    },
  ],
});
```

## Configuration options

`defineContract` accepts these options:

| Option         | Type                                                  | Default                                            | Description                                                                                                       |
| -------------- | ----------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `name`         | `string`                                              | *required*                                         | Stable identifier for the contract. Appears on every event and joins to per-contract aggregates on the dashboard. |
| `schema`       | `ZodType<T>`                                          | *required*                                         | Your Zod schema (v3 or v4).                                                                                       |
| `rules`        | `Rule<T>[]`                                           | `[]`                                               | Named domain rules — see [Rules](/concepts/rules).                                                                |
| `retry`        | `{ maxAttempts?, backoff?, baseMs? }`                 | `{ maxAttempts: 3, backoff: "none", baseMs: 200 }` | How aggressively to retry on failures.                                                                            |
| `repairs`      | `Partial<Record<FailureCategory, RepairFn \| false>>` | —                                                  | Override or disable the built-in repair message for a specific failure category.                                  |
| `instructions` | `{ suffix?: string }`                                 | —                                                  | Append extra text to the schema-derived prompt.                                                                   |
| `onAttempt`    | `(event) => void`                                     | —                                                  | Per-attempt hook for logging or metrics.                                                                          |
| `logger`       | `ContractLogger<T>`                                   | —                                                  | Lifecycle hooks — pair with `createBoundaryLogger` for the dashboard, or roll your own.                           |
| `model`        | `string`                                              | —                                                  | Default model label, stamped on every event. Override per-call via `contract.accept(run, { model })`.             |
| `debug`        | `boolean`                                             | `false`                                            | Verbose console logging during execution.                                                                         |

## Next steps

<CardGroup cols={2}>
  <Card title="Rules" icon="scale-balanced" href="/concepts/rules">
    Write domain correctness rules
  </Card>

  <Card title="The Repair Loop" icon="rotate" href="/concepts/repair-loop">
    How Boundary turns failures into fixes
  </Card>
</CardGroup>

## Observability

Start with [local development logging](/guides/local-development) while you are writing a contract. When the contract is running in production, add [production observability](/guides/production-observability) to track acceptance rate, failure categories, retries, and failing rules across real traffic.
