Vercel Logo

Keep policy out of the prompt

Vendor Review has rules the company can state exactly:

  • Requests at or above a defined annual cost require Procurement review
  • Requests involving restricted data require Security review
  • Requests missing required fields cannot proceed

Company policy lives in code. The model does not get a vote. Open the unfinished lib/policy.ts from the scaffold and implement both rules.

export function routeByPolicy(
  request: VendorRequestInput
): PolicyRoute {
  const reviewers = new Set<ReviewerGroup>();
  const reasons: string[] = [];
 
  if (request.annualCost >= 50_000) {
    reviewers.add("procurement");
    reasons.push("Annual cost is at least $50,000");
  }
  if (request.dataTypes.includes("restricted")) {
    reviewers.add("security");
    reasons.push(
      "Vendor will handle restricted company data"
    );
  }
 
  return {
    requiresHumanReview: reviewers.size > 0,
    reviewerGroups: [...reviewers],
    reasons,
    policyVersion: "vendor-routing-v1"
  };
}

The model handles work that benefits from judgment: classifying the vendor category, identifying missing context in the business purpose, and describing concerns for a reviewer.

Define that boundary with structured output:

const assessmentSchema = z.object({
  category: z.enum([
    "productivity",
    "development",
    "data",
    "security",
    "other"
  ]),
  suggestedRisk: z.enum(["low", "medium", "high"]),
  missingInformation: z.array(z.string()).max(5),
  summary: z.string()
});
 
const result = await generateText({
  model: ASSESSMENT_MODEL,
  instructions: assessmentInstructions,
  prompt: formatRequestForAssessment(request),
  output: Output.object({ schema: assessmentSchema })
});
 
return result.output;

The complete implementation reads the validated object from result.output in lib/ai.ts. Do not parse result.text or treat an unvalidated string as the assessment.

Structured output constrains the response shape. It does not guarantee correct judgment, prevent prompt injection, or authorize an action. Those concerns remain separate.

Combine without hiding the source

The API response should preserve policy and assessment as distinct objects. Do not collapse them into one unexplained risk field.

policy:       cost threshold → Procurement review
assessment:   data vendor, medium suggested risk, missing retention details
final route:  waiting for Procurement

Write down who decides what

Complete Who decides what in docs/readiness.md. Put each current behavior under deterministic policy, model judgment, or human authority. If the team cannot place it, the feature is not ready to build.

Summary

Use code for rules the company can state exactly. Use a model for ambiguity and judgment. Use an authorized person for final approval. Keeping those sources separate makes the result easier to test, explain, and change.

Check your work

Pass a $50,000 request with restricted data to routeByPolicy(). The result should require both Procurement and Security, include two plain-language reasons, and name vendor-routing-v1. Change the model’s suggested risk in your head. The route should not move.

Compare your implementation with lib/policy.ts on complete.

Was this helpful?

supported.