Skip to main content
Creates a reusable contract that validates LLM outputs against a schema and rules.

Signature

function defineContract<T>(
  config: ContractConfig<T>
): DefinedContract<T>

ContractConfig<T>

FieldTypeRequiredDefaultDescription
namestringYesStable identifier for the contract. Appears on every event and joins to per-contract aggregates on the dashboard.
schemaZodType<T>YesZod schema (v3 or v4) defining the expected output structure
rulesRule<T>[]No[]Named domain rules — see Rules for the full shape
retryRetryOptionsNo{ maxAttempts: 3, backoff: "none", baseMs: 200 }Retry behavior on failure
repairsRepairOverridesNoCustom repair strategies per failure category
instructions{ suffix?: string }NoAppend text to auto-generated schema prompt
onAttemptAttemptHookNoCalled after each attempt with { ok, category, issues, durationMS, ... }
loggerContractLogger<T>NoLifecycle hooks. Pair with createBoundaryLogger(...) for the dashboard.
modelstringNoDefault model label, stamped on every event. Override per-call via contract.accept(run, { model }).
debugbooleanNofalseVerbose console logging via createConsoleLogger()

DefinedContract<T>

The returned contract object has two methods:

.accept(run, overrides?)

contract.accept(
  run: RunFn,
  overrides?: Partial<ContractConfig<T>>
): Promise<ContractResult<T>>
Executes your LLM call inside the contract boundary. Returns a ContractResult<T>. The optional overrides let you override any config option at call time. Definition-time options are the defaults; runtime options take precedence.

.describe()

contract.describe(): {
  schema: SchemaField[]
  rules: RuleDefinition[]
}
Returns the schema and rule metadata Boundary derived from the contract. The SDK logger uses this metadata for dashboard attribution.

RetryOptions

type RetryOptions = {
  maxAttempts?: number    // default: 3
  backoff?: "none" | "linear" | "exponential"  // default: "none"
  baseMs?: number         // default: 200
}
StrategyDelay pattern
"none"No delay between retries
"linear"baseMs * attemptNumber
"exponential"baseMs * 2^attemptNumber

RepairOverrides

Override or disable repair for specific failure categories:
type RepairOverrides = Partial<Record<
  FailureCategory,
  RepairFn | false
>>

type RepairFn = (detail: AttemptDetail) => Message[]
Set to false to skip retrying for that category. Provide a function to generate custom repair messages.

Example

import { z } from "zod";
import { defineContract } from "@withboundary/contract";

const contract = 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",
    },
  ],
  retry: { maxAttempts: 3 },
  repairs: {
    REFUSAL: false,  // don't retry on refusal
  },
});

const result = await contract.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;
});

Runtime overrides

Override config per-call without changing the contract definition:
// use more retries for this specific call
const result = await contract.accept(run, {
  retry: { maxAttempts: 5 },
});