Skip to main content
Schemas say what data looks like. Rules say whether it can be trusted. A rule is a plain function that checks whether an LLM output is correct for your domain — not just structurally valid.

Rule signature

type Rule<T> = (data: T) => true | string
Return true if the data is correct. Return a string describing the violation if it’s wrong.
// passes
(lead) => lead.tier === "hot" ? lead.score > 70 : true

// fails — returns the violation message
(lead) => lead.reason.length > 0 || "reason cannot be empty"
That violation string is sent directly to the model as repair context. The more specific the message, the better the repair.

Rules are sync, deterministic, and cheap

Three constraints on rules in v1:
No async rules. Rules run synchronously during validation. If you need async checks (database lookups, API calls), do them before or after the contract.
Same input, same output. No randomness, no side effects, no external state. This makes contracts testable and reproducible.
Rules run on every attempt (potentially 2-3 times per contract call). Keep them fast — in-memory checks only. No database queries, no network calls.

Domain examples

Lead scoring

rules: [
  // if tier is "hot", score must be above 70
  (d) => d.tier === "hot" ? d.score > 70 : true,
  // reason is always required
  (d) => d.reason.length > 0 || "reason cannot be empty",
]

Finance

rules: [
  // subtotal + tax must equal total
  (d) => Math.abs(d.subtotal + d.tax - d.total) < 0.01
    || `subtotal + tax != total (${d.subtotal} + ${d.tax} = ${d.subtotal + d.tax}, got ${d.total})`,
  // currency must match the account
  (d) => d.currency === d.accountCurrency
    || `currency mismatch: got ${d.currency}, expected ${d.accountCurrency}`,
]

Support ops

rules: [
  // high priority tickets require an escalation reason
  (d) => d.priority !== "high" || d.reason.length > 0
    || "high priority requires escalation reason",
]

Agents

rules: [
  // cannot close a ticket that's still in review
  (d) => !(d.action === "close_ticket" && d.status === "needs_review")
    || "cannot close a ticket with status needs_review",
]

Compliance

rules: [
  // US users require SSN
  (d) => d.country !== "US" || d.ssn !== null
    || "US users require SSN",
  // cannot auto-approve without verified identity
  (d) => !d.approved || d.identityVerified
    || "cannot auto-approve without verified identity",
]

String messages drive repair

The exact string you return from a failed rule becomes part of the repair prompt sent to the model. This is why specific messages matter:
(d) => d.score > 70 || "invalid"
The model gets: “invalid” — not enough to fix anything.

Multiple rules

Rules are evaluated in order. All failing rules are collected — Boundary does not short-circuit on the first failure. All violations are sent to the model in a single repair message.
rules: [
  (d) => d.tier === "hot" ? d.score > 70 : true,
  (d) => d.reason.length > 0 || "reason cannot be empty",
  (d) => d.score >= 0 && d.score <= 100 || "score must be between 0 and 100",
]
// If all three fail, the model gets all three violations at once

Next steps

The Repair Loop

How violations become targeted fixes

Results

What you get back from a contract call