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

Extracting text from Claude’s response

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.

Using tool_use for structured output

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

ModelBest for
claude-sonnet-4-20250514Best balance of quality and speed for structured output
claude-haiku-4-5-20251001Fastest and cheapest — good for high-volume, simpler schemas
claude-opus-4-6Most capable — use for complex rules and difficult domains