> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withboundary.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add Boundary to an LLM Feature

> Put a contract boundary around an existing model call

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.

<Prompt description="Install the Boundary skill before asking an AI coding agent to add this pattern." actions={["copy", "cursor"]}>
  npx skills add [https://docs.withboundary.com](https://docs.withboundary.com)
</Prompt>

## Before Boundary

A typical structured-output flow looks like this:

```typescript theme={null}
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:

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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.

<Tabs>
  <Tab title="Weak">
    ```typescript theme={null}
    check: (lead) => lead.score >= 70 || "invalid score"
    ```
  </Tab>

  <Tab title="Useful">
    ```typescript theme={null}
    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`
    ```
  </Tab>
</Tabs>

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:

```typescript theme={null}
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

| Mistake                                        | Fix                                                            |
| ---------------------------------------------- | -------------------------------------------------------------- |
| Returning parsed JSON from the `RunFn`         | Return the raw provider text and let Boundary parse it         |
| Forgetting `attempt.repairs`                   | Spread repair messages into retry prompts                      |
| Treating rejected `cleaned` data as a fallback | Send rejected runs to review or fail the request               |
| Writing rules that call a database or API      | Fetch that context before the contract; keep rules synchronous |
| Using vague rule failures                      | Include the actual value and expected condition                |

## Next steps

<CardGroup cols={2}>
  <Card title="Local development" icon="terminal" href="/guides/local-development">
    Debug the contract before production
  </Card>

  <Card title="OpenAI integration" icon="bolt" href="/guides/openai">
    Provider-specific OpenAI example
  </Card>

  <Card title="Anthropic integration" icon="brain" href="/guides/anthropic">
    Provider-specific Claude example
  </Card>

  <Card title="Production observability" icon="chart-line" href="/guides/production-observability">
    Send accepted and rejected runs to the dashboard
  </Card>
</CardGroup>
