Skip to main content
This quickstart uses the free @withboundary/contract package. It does not require a Boundary account, API key, or hosted dashboard.
Using an AI coding assistant? Install the Boundary skill first so it can find the LLM output path, add the contract at the trust boundary, and verify accepted, repaired, and rejected outcomes.

Install the Boundary skill in your AI coding agent.

Open in Cursor

Install

npm install @withboundary/contract zod

Define what correct means

A contract is a schema plus rules. The schema checks the shape. Rules check the values your product cares about.
lead-contract.ts
import { z } from "zod";
import { defineContract } from "@withboundary/contract";

const leadSchema = z.object({
  tier: z.enum(["hot", "warm", "cold"]),
  score: z.number().min(0).max(100),
  reason: z.string(),
});

export const leadContract = defineContract({
  name: "lead-scoring",
  schema: leadSchema,
  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}; set tier to warm/cold or raise score to at least 70`,
    },
    {
      name: "reason_required",
      description: "Every scoring decision must explain why",
      fields: ["reason"],
      check: (lead) =>
        lead.reason.trim().length > 0 || "reason must not be empty",
    },
  ],
  debug: true,
});
debug: true turns on the built-in console logger. It prints attempts, failures, repairs, and the final result locally. No data leaves your process.

Wrap your model call

contract.accept() takes your existing LLM call as a function. Return the raw model output as a string. Boundary parses it, validates it, repairs failures when possible, and returns a typed result.
score-lead.ts
import { leadContract } from "./lead-contract";

const result = await leadContract.accept(async (attempt) => {
  const response = await callYourLLM({
    messages: [
      {
        role: "user",
        content: [
          "Score this lead as JSON.",
          attempt.instructions,
          "Lead: signed up two days ago, visited pricing three times, opened one email.",
        ].join("\n\n"),
      },
      ...attempt.repairs,
    ],
  });

  return response.text;
});
On the first attempt, attempt.repairs is empty. If the output fails, Boundary builds repair messages from the schema and rule violations. Your next model call receives those messages.

Handle the result

if (result.ok) {
  await crm.updateLead(leadId, result.data);
} else {
  const lastAttempt = result.error.attempts.at(-1);

  console.error("Lead scoring rejected:", {
    message: result.error.message,
    category: lastAttempt?.category,
    issues: lastAttempt?.issues,
  });

  await reviewQueue.add({
    leadId,
    reason: result.error.message,
    attempts: result.error.attempts,
  });
}
When result.ok is true, result.data is typed from your schema and every rule has passed. When result.ok is false, no data is returned. You decide whether to fail the request, retry with different context, or send it to a human review queue.

What local debugging shows

With debug: true, a failed first attempt gives you the important parts:
[contract] lead-scoring attempt 1 failed RULE_ERROR
[contract] hot_requires_high_score: tier is "hot" but score is 25; set tier to warm/cold or raise score to at least 70
[contract] reason_required: reason must not be empty
[contract] retrying with repair context
[contract] lead-scoring accepted on attempt 2
That is enough to tune rules, prompts, and reject handling before you wire the hosted dashboard.

Next steps

Local development

Use console logging, tests, and custom hooks without an API key

Add Boundary to an LLM feature

Adapt this pattern to your existing provider code

OpenAI guide

Use Boundary with OpenAI structured outputs

Production observability

Send contract events to the hosted dashboard