Boundary works with Anthropic’s Claude models. Claude returns text or tool results; Boundary validates the output against your schema and rules.
Install
npm install @boundary/contract zod @anthropic-ai/sdk
Basic integration
import { z } from "zod";
import { defineContract } from "@boundary/contract";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
// 1. Define your schema
const schema = z.object({
subtotal: z.number(),
tax: z.number(),
total: z.number(),
currency: z.string(),
});
// 2. Define your contract
const contract = defineContract({
schema,
rules: [
(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})`,
],
});
// 3. Run with Anthropic inside the contract
const result = await contract.accept(async (attempt) => {
const res = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: attempt.instructions,
messages: [
{
role: "user",
content: "Extract the invoice totals from this text: Subtotal $100.00, Tax $8.50, Total $108.50, USD",
},
...attempt.repairs.map((r) => ({
role: r.role as "user" | "assistant",
content: r.content,
})),
],
});
// Extract text from Claude's response
const block = res.content[0];
return block.type === "text" ? block.text : null;
});
if (result.ok) {
console.log("Accepted:", result.data);
// { subtotal: 100, tax: 8.5, total: 108.5, currency: "USD" }
} else {
console.error("Failed:", result.error.message);
}
Claude’s messages.create() returns a response with a content array. Each block has a type:
// For text responses
const block = res.content[0];
return block.type === "text" ? block.text : null;
The RunFn must return a string (or null). Always extract the text content from Claude’s response object before returning.
Claude’s tool use feature can enforce JSON structure. Combine it with Boundary for value correctness:
const result = await contract.accept(async (attempt) => {
const res = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: "Extract invoice data. Use the provided tool.",
messages: [
{ role: "user", content: "Subtotal $100.00, Tax $8.50, Total $108.50, USD" },
...attempt.repairs.map((r) => ({
role: r.role as "user" | "assistant",
content: r.content,
})),
],
tools: [
{
name: "extract_invoice",
description: "Extract invoice totals",
input_schema: {
type: "object",
properties: {
subtotal: { type: "number" },
tax: { type: "number" },
total: { type: "number" },
currency: { type: "string" },
},
required: ["subtotal", "tax", "total", "currency"],
},
},
],
tool_choice: { type: "tool", name: "extract_invoice" },
});
// Extract the tool input as JSON string
const toolBlock = res.content.find((b) => b.type === "tool_use");
return toolBlock ? JSON.stringify(toolBlock.input) : null;
});
How repair context maps to Anthropic messages
Boundary’s attempt.repairs are Message objects with role and content. Map them to Anthropic’s message format:
...attempt.repairs.map((r) => ({
role: r.role as "user" | "assistant",
content: r.content,
}))
Repair messages typically have role: "user" — they contain the violations and instructions for the model to fix its output.
Model selection
| Model | Best for |
|---|
claude-sonnet-4-20250514 | Best balance of quality and speed for structured output |
claude-haiku-4-5-20251001 | Fastest and cheapest — good for high-volume, simpler schemas |
claude-opus-4-6 | Most capable — use for complex rules and difficult domains |