Skip to main content
OpenAI structured outputs help keep the response JSON-shaped. Boundary applies your schema and domain rules before that JSON reaches application state.

Install

npm install @withboundary/contract zod openai

Define the contract

import { z } from "zod";
import { defineContract } from "@withboundary/contract";
import OpenAI from "openai";

const openai = new OpenAI();

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

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}; lower the tier or raise score to at least 70`,
    },
    {
      name: "reason_required",
      description: "Every scoring decision must have a non-empty reason",
      fields: ["reason"],
      check: (lead) =>
        lead.reason.trim().length > 0 || "reason must explain the score",
    },
  ],
});

Call OpenAI inside accept()

Pass attempt.instructions to the model and append attempt.repairs. On the first attempt, repairs is empty. On retries, it contains the schema and rule failures from the previous attempt.
const result = await leadContract.accept(async (attempt) => {
  const response = await openai.responses.create({
    model: process.env.OPENAI_MODEL ?? "gpt-4.1",
    input: [
      {
        role: "user",
        content: [
          "Score this lead as JSON.",
          attempt.instructions,
          `Lead: ${leadSummary}`,
        ].join("\n\n"),
      },
      ...attempt.repairs,
    ],
    text: {
      format: {
        type: "json_schema",
        name: "lead_score",
        schema: {
          type: "object",
          additionalProperties: false,
          properties: {
            tier: { type: "string", enum: ["hot", "warm", "cold"] },
            score: { type: "number", minimum: 0, maximum: 100 },
            reason: { type: "string" },
          },
          required: ["tier", "score", "reason"],
        },
      },
    },
  });

  return response.output_text;
});
Return the raw response text. Boundary handles parsing, validation, repair, and the final accept/reject decision.

Gate side effects

if (result.ok) {
  await crm.updateLead(leadId, result.data);
  return { status: "accepted", lead: result.data };
}

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

return { status: "needs_review" };
Do not fall back to the last parsed attempt when ok is false. The rejected output failed the contract.

Chat Completions

If you are using Chat Completions, the same pattern applies. Put attempt.instructions in the system message and map repair messages into the provider’s message shape.
const result = await leadContract.accept(async (attempt) => {
  const response = await openai.chat.completions.create({
    model: process.env.OPENAI_MODEL ?? "gpt-4.1",
    messages: [
      { role: "system", content: attempt.instructions },
      { role: "user", content: `Score this lead: ${leadSummary}` },
      ...attempt.repairs.map((repair) => ({
        role: repair.role as "user" | "assistant",
        content: repair.content,
      })),
    ],
    response_format: { type: "json_object" },
  });

  return response.choices[0]?.message?.content ?? null;
});

What Boundary adds

Use OpenAI structured output support for response shape. Use Boundary for the deterministic decision your app can trust:
  • cross-field rules such as “hot tier requires score >= 70”
  • repair messages built from actual schema and rule failures
  • a typed ContractResult that separates accepted data from rejection history
  • local logging during development and hosted traces when you add the SDK
Next: Add Boundary to an LLM feature for the provider-agnostic workflow, or Production observability when you want dashboard traces.