Skip to main content

The problem

Your email parser returns this:
{
  "sender": "jane@acme.com",
  "subject": "URGENT: Server down in production",
  "intent": "urgent_support",
  "priority": "low",
  "department": "sales",
  "summary": "Server issue"
}
The JSON is valid. The values are not. An urgent support email should not be low priority, routed to sales, or summarized so briefly that the receiving team has no context.

The contract

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

export const emailSchema = z.object({
  sender: z.string().email(),
  subject: z.string(),
  intent: z.enum([
    "general_inquiry",
    "billing",
    "urgent_support",
    "feature_request",
    "cancellation",
  ]),
  priority: z.enum(["low", "medium", "high", "critical"]),
  department: z.enum(["sales", "support", "engineering", "billing", "product"]),
  summary: z.string(),
});

export const emailContract = defineContract({
  name: "email-parsing",
  schema: emailSchema,
  rules: [
    {
      name: "urgent_requires_high_priority",
      description: "Urgent intents must be high or critical priority",
      fields: ["intent", "priority"],
      check: (email) =>
        !email.intent.includes("urgent") || ["high", "critical"].includes(email.priority)
          || `intent is "${email.intent}" but priority is "${email.priority}"; use high or critical`,
    },
    {
      name: "support_routed_to_support",
      description: "Support intents route to support or engineering",
      fields: ["intent", "department"],
      check: (email) =>
        !email.intent.includes("support") || ["support", "engineering"].includes(email.department)
          || `support intent routed to "${email.department}"; use support or engineering`,
    },
    {
      name: "cancellation_routed_correctly",
      description: "Cancellation requests route to billing or support",
      fields: ["intent", "department"],
      check: (email) =>
        email.intent !== "cancellation" || ["billing", "support"].includes(email.department)
          || `cancellation routed to "${email.department}"; use billing or support`,
    },
    {
      name: "summary_substantive",
      description: "Summary has enough context for the receiving team",
      fields: ["summary"],
      check: (email) =>
        email.summary.trim().length >= 20
          || "summary must include enough context for the receiving team",
    },
  ],
});

Run the model

const result = await emailContract.accept(async (attempt) => {
  const response = await callYourLLM({
    messages: [
      {
        role: "user",
        content: [
          "Parse this email as JSON.",
          attempt.instructions,
          `From: ${email.from}`,
          `Subject: ${email.subject}`,
          `Body: ${email.body}`,
        ].join("\n\n"),
      },
      ...attempt.repairs,
    ],
  });

  return response.text;
});

Accept or reject

if (result.ok) {
  await ticketSystem.create({
    from: result.data.sender,
    priority: result.data.priority,
    department: result.data.department,
    summary: result.data.summary,
  });

  return { status: "created" };
}

logger.warn("email parse rejected", {
  emailId,
  attempts: result.error.attempts.length,
  lastFailure: result.error.attempts.at(-1),
});

await triageQueue.add({
  emailId,
  reason: result.error.message,
  attempts: result.error.attempts,
});

return { status: "needs_triage" };
Rejected parses can be reviewed, retried with more source context, or dropped according to your support workflow. They should not create tickets with guessed routing.

When to use this pattern

  • Inbound email triage and routing
  • Customer support ticket classification
  • Email-to-CRM extraction
  • Invoice or contract emails with structured attachments
  • Digest generation where routing or priority matters