Skip to main content
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

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.
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:
FieldTypeDescription
attemptnumberCurrent attempt number (1-indexed)
maxAttemptsnumberMaximum retries allowed
instructionsstringAuto-generated prompt from your schema
repairsArray<{ role, content }>Repair messages from prior failures — empty on the first attempt, always role: "user" on retries. See the attempt.repairs field.

The RunFn

Your function must match this signature:
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.
The RunFn must return a string, not a parsed object. Extract the text content from your provider’s response object before returning.

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.
// 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:
OptionTypeDefaultDescription
namestringrequiredStable identifier for the contract. Appears on every event and joins to per-contract aggregates on the dashboard.
schemaZodType<T>requiredYour Zod schema (v3 or v4).
rulesRule<T>[][]Named domain rules — see Rules.
retry{ maxAttempts?, backoff?, baseMs? }{ maxAttempts: 3, backoff: "none", baseMs: 200 }How aggressively to retry on failures.
repairsPartial<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) => voidPer-attempt hook for logging or metrics.
loggerContractLogger<T>Lifecycle hooks — pair with createBoundaryLogger for the dashboard, or roll your own.
modelstringDefault model label, stamped on every event. Override per-call via contract.accept(run, { model }).
debugbooleanfalseVerbose console logging during execution.

Next steps

Rules

Write domain correctness rules

The Repair Loop

How Boundary turns failures into fixes

Observability

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