Skip to main content
Boundary is easiest to add when you treat it as the last step before your application trusts model output. You do not need to move provider code into Boundary. Keep your model call where it is. Wrap the call with contract.accept(), return the provider’s raw text, and only continue with result.data when the contract accepts.

Install the Boundary skill before asking an AI coding agent to add this pattern.

Open in Cursor

Before Boundary

A typical structured-output flow looks like this:
const response = await callYourLLM(prompt);
const parsed = JSON.parse(response.text);

await crm.updateLead(leadId, parsed);
That code checks whether the model returned JSON. It does not check whether the values are safe for the workflow.

Add a contract near the workflow

Keep the contract close to the feature it protects:
import { z } from "zod";
import { defineContract } from "@withboundary/contract";

export const leadContract = 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}; 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",
    },
  ],
});
Write rules for the parts that would cause bad behavior if they were wrong: routing, money, permissions, state transitions, risk labels, or customer-facing decisions.

Wrap the provider call

Inside accept(), call your provider the same way you already do. Include attempt.instructions and spread attempt.repairs into later attempts.
const result = await leadContract.accept(async (attempt) => {
  const response = await callYourLLM({
    messages: [
      {
        role: "user",
        content: [
          "Return a lead score as JSON.",
          attempt.instructions,
          `Lead details: ${leadDescription}`,
        ].join("\n\n"),
      },
      ...attempt.repairs,
    ],
  });

  return response.text;
});
The important rule: return a string, not a parsed object. Boundary owns parsing, validation, and repair.

Gate the side effect on result.ok

Only accepted data should reach the system Boundary is protecting.
if (result.ok) {
  await crm.updateLead(leadId, result.data);
  return { status: "accepted", lead: result.data };
}

const lastAttempt = result.error.attempts.at(-1);

logger.warn("lead scoring rejected", {
  leadId,
  category: lastAttempt?.category,
  issues: lastAttempt?.issues,
});

await reviewQueue.add({
  leadId,
  reason: result.error.message,
  attempts: result.error.attempts,
});

return { status: "needs_review" };
Do not write result.error.attempts.at(-1)?.cleaned to your database as a fallback. The whole point of the boundary is that rejected data does not enter the application path.

Write repair-friendly rule messages

A good rule failure tells the model what was wrong and what would satisfy the rule.
check: (lead) => lead.score >= 70 || "invalid score"
The useful version includes the field, actual value, expected condition, and a valid repair direction.

Start with local logging

Use debug: true or createConsoleLogger while you are integrating:
export const leadContract = defineContract({
  name: "lead-scoring",
  schema,
  rules,
  debug: process.env.NODE_ENV !== "production",
});
Once the contract is stable, remove noisy console logging and add either a lightweight ContractLogger for your own metrics or the hosted SDK for dashboard traces.

Common integration mistakes

MistakeFix
Returning parsed JSON from the RunFnReturn the raw provider text and let Boundary parse it
Forgetting attempt.repairsSpread repair messages into retry prompts
Treating rejected cleaned data as a fallbackSend rejected runs to review or fail the request
Writing rules that call a database or APIFetch that context before the contract; keep rules synchronous
Using vague rule failuresInclude the actual value and expected condition

Next steps

Local development

Debug the contract before production

OpenAI integration

Provider-specific OpenAI example

Anthropic integration

Provider-specific Claude example

Production observability

Send accepted and rejected runs to the dashboard