Free lesson · GenAI Platform Engineering

Build prompt approval workflow with evaluation gate

Implement a promotion pipeline where prompts must pass automated evaluation benchmarks and receive team-lead approval before becoming the production default.

Course: AI Developer Platform Engineering · Chapter 15 · Prompt Engineering Workspace

Free to read — no subscription required.

Introduction

When you build prompt engineering workflows in a team environment, merging an untested or unsafe prompt directly into production can silently degrade model quality, inflate costs, or introduce injection vulnerabilities. A structured approval workflow solves this by requiring every prompt version to clear automated eval gates—quality score thresholds, safety scans, and cost estimates—before a designated reviewer gives final sign-off. By the end of this lesson, you'll be able to implement an eval-gated approval pipeline that runs automated checks, routes prompts to human reviewers, and transitions approved versions to ACTIVE status in the prompt registry.

Key Terminology

  • Eval Gate — The automated quality check implemented by PromptEvalGate that renders a prompt template against an evaluation dataset, collects float scores from the eval runner, and compares the average to a configurable threshold (default 0.8); if the average falls below the threshold, the stage fails and the pipeline halts without proceeding further.
  • StageResult — A Pydantic model that records the outcome of a single pipeline stage, capturing stage, passed, an optional float score, and a human-readable details string; the orchestrator appends one StageResult per stage to the PromptApproval as the pipeline runs.
  • PromptApproval — The accumulating record for an entire approval run, holding all StageResult objects across every stage plus the final reviewer decision (reviewer, reviewer_comment, and approved); it is the artifact the prompt registry reads to determine whether to transition a version from REVIEW to ACTIVE.
  • Safety Scanner — The PromptSafetyScanner class that tests the combined prompt and system message against a list of regex patterns—such as ignore previous instructions and deeply nested variable expressions—to detect prompt injection attempts; any single match fails the SAFETY_CHECK stage outright.
  • Eval Runner — A duck-typed dependency injected into PromptEvalGate that must implement .generate(prompt, model) to produce model responses and .score(response, expected) to return a float quality score; swapping the eval runner (e.g., for a mock in tests) changes the scoring backend without touching the gate logic.

Concepts

Automated Gates as a Quality Firewall

A prompt that reaches a human reviewer carries an implicit claim: someone believed it was worth their attention. In a team workflow, that assumption is too fragile. An untested prompt can silently degrade model responses, surface injection vulnerabilities, or inflate inference costs—and none of those failures announce themselves until they reach production. The four-stage pipeline (eval gate → safety check → cost review → human review) acts as a quality firewall that demands structured, recorded evidence before a reviewer is asked to make a judgment call.

The key mental model is ordered gates with early halt: if any automated stage returns passed=False, the pipeline stops immediately and skips the remaining stages. Reviewers never receive a PromptApproval that hasn't already cleared every preceding check—their attention is reserved for prompts that have earned it (see Code Walkthrough for how the orchestrator accumulates StageResult objects and short-circuits on failure).

Loading diagram...

Statistical Scoring vs. Pattern Matching

The two automated checks in this lesson operate on fundamentally different principles, and understanding the distinction matters because they guard against different risks.

The eval gate (PromptEvalGate) is statistical. It renders the prompt template against each sample in the evaluation dataset, collects a float quality score from the eval runner per response, then computes an average across the full dataset. That average is compared against a threshold—0.8 by default. A single weak response lowers the average without being definitive; a strong average across many samples is meaningful empirical evidence. This gate guards against quality regression.

The safety scanner (PromptSafetyScanner) is binary regex matching. It tests the combined prompt and system message against a curated list of known-dangerous patterns. Any single match fails the stage—there is no averaging, no partial credit, and no threshold to tune. This gate guards against safety violations like prompt injection phrases and structural anomalies that indicate an attempt to subvert the model's instructions. Neither check substitutes for the other.

From Accumulated Evidence to Registry Promotion

Each stage appends a StageResult to the PromptApproval.stages list as the pipeline progresses. By the time a prompt reaches the human-review stage, the reviewer holds a complete evidence record: every automated score, every flagged pattern (or confirmed absence), and the number of evaluation samples scored. The reviewer adds a comment and sets approved directly on the PromptApproval.

That final decision is what the prompt registry acts on. A version begins the workflow in REVIEW status; a human approval on a fully-passed PromptApproval is the event that transitions it to ACTIVE. Any automated failure or reviewer rejection leaves the version in REVIEW—no silent promotion, no ambiguity about what cleared the bar and what did not.

Code Walkthrough

Now that you understand the four-stage architecture—eval gate, safety check, cost estimator, and human review—and how each stage feeds into the prompt registry and audit log, let's build the pipeline in code.

Data Models

The Pydantic models below define the approval workflow's structure. ApprovalStage enumerates every step; StageResult captures the outcome of each step; PromptApproval accumulates results across the full pipeline and records the final reviewer decision.

Code snippetpython
1from __future__ import annotations 2from datetime import datetime 3from enum import Enum 4from typing import Optional 5from pydantic import BaseModel, Field 6 7class ApprovalStage(str, Enum): 8 EVAL_GATE = "eval_gate" 9 SAFETY_CHECK = "safety_check" 10 COST_REVIEW = "cost_review" 11 HUMAN_REVIEW = "human_review" 12 13class StageResult(BaseModel): 14 stage: ApprovalStage 15 passed: bool 16 score: Optional[float] = None 17 details: str 18 completed_at: datetime = Field(default_factory=datetime.utcnow) 19 20class PromptApproval(BaseModel): 21 approval_id: str 22 prompt_id: str 23 version_id: str 24 requested_by: str 25 stages: list[StageResult] = Field(default_factory=list) 26 reviewer: Optional[str] = None 27 reviewer_comment: Optional[str] = None 28 approved: Optional[bool] = None 29 created_at: datetime = Field(default_factory=datetime.utcnow)

Eval Gate

The PromptEvalGate class implements the automated quality gate. Its evaluate method renders the prompt template against each sample in the evaluation dataset, sends the rendered prompt to the model via the eval runner, scores each response against the expected output, and returns a StageResult with the average score and a pass/fail verdict relative to the configurable threshold. If the average score falls below threshold, the stage fails and the workflow returns early without proceeding to human review.

Code snippetpython
1import re 2from dataclasses import dataclass, field 3 4@dataclass 5class PromptEvalGate: 6 eval_runner: object # duck-typed: .generate(prompt, model) / .score(response, expected) 7 threshold: float = 0.8 8 9 async def evaluate( 10 self, 11 prompt_content: str, 12 model_id: str, 13 dataset: list[dict], # [{"variables": {...}, "expected_output": "..."}] 14 ) -> StageResult: 15 scores: list[float] = [] 16 17 for sample in dataset: 18 # Naive template interpolation; replace with InterpolationEngine in production 19 rendered = prompt_content.format(**sample["variables"]) 20 response = await self.eval_runner.generate(rendered, model_id) 21 score = await self.eval_runner.score(response, sample["expected_output"]) 22 scores.append(score) 23 24 avg = sum(scores) / len(scores) if scores else 0.0 25 passed = avg >= self.threshold 26 27 return StageResult( 28 stage=ApprovalStage.EVAL_GATE, 29 passed=passed, 30 score=avg, 31 details=( 32 f"Average score: {avg:.3f} (threshold: {self.threshold}). " 33 f"{len(scores)} samples evaluated." 34 ), 35 ) 36 37@dataclass 38class PromptSafetyScanner: 39 DANGEROUS_PATTERNS: list[str] = field(default_factory=lambda: [ 40 r"ignore previous instructions", 41 r"disregard your system prompt", 42 r"\{\{.*\{\{", # deeply nested variable expressions 43 ]) 44 45 def scan(self, prompt_content: str, system_message: str = "") -> StageResult: 46 issues: list[str] = [] 47 combined = f"{prompt_content}\n{system_message}".lower() 48 49 for pattern in self.DANGEROUS_PATTERNS: 50 if re.search(pattern, combined): 51 issues.append(pattern) 52 53 passed = len(issues) == 0 54 return StageResult( 55 stage=ApprovalStage.SAFETY_CHECK, 56 passed=passed, 57 details=( 58 "No dangerous patterns detected." 59 if passed 60 else f"Flagged patterns: {issues}" 61 ), 62 )

The PromptSafetyScanner.scan method tests each regex pattern against the combined prompt and system message. Any match is collected as an issue; a non-empty issue list fails the stage and prevents promotion to ACTIVE.

Running the Pipeline

With both gate classes in place, the orchestrator accumulates StageResult objects into a PromptApproval, halts early on any failure, and—on full automated pass—routes the approval record to the human-review step where a designated reviewer inspects the results and provides a final decision before the prompt registry updates the version status from REVIEW to ACTIVE.

Verify by instantiating PromptEvalGate with a mock eval runner, calling evaluate on a small dataset where all scores exceed the threshold, and confirming the returned StageResult has passed=True and a score value at or above your configured threshold.

Do's and Don'ts

Having walked through the gate classes and how reviewers consume the accumulated StageResult evidence, the rules below capture the practices that keep that pipeline trustworthy.

Do's

  1. Do set PromptEvalGate.threshold explicitly for your dataset — the default of 0.8 is a starting point, not a contract; calibrating it against your evaluation dataset before wiring the gate into the pipeline ensures that the StageResult.passed verdict actually reflects acceptable quality, rather than an arbitrary number that silently passes degraded prompts.
  2. Do pass both prompt_content and system_message into PromptSafetyScanner.scan — the scanner concatenates both fields into a single combined string before testing each regex; omitting the system message means patterns like "disregard your system prompt" or deeply nested {{.*{{ expressions hidden there bypass the check entirely and reach human review undetected.
  3. Do return early from the orchestrator on the first StageResult where passed=False — halting before SAFETY_CHECK, COST_REVIEW, or HUMAN_REVIEW when the eval gate fails keeps the reviewer queue free of prompts that didn't clear automated checks, and prevents a failing PromptApproval record from accumulating misleading partial-pass stage results.

Don'ts

  1. Don't use prompt_content.format(**sample["variables"]) in production — this naive string interpolation silently corrupts prompts that contain literal curly braces, percent signs, or user-supplied text with format-string syntax; the walkthrough explicitly marks it as a placeholder and instructs you to replace it with a proper InterpolationEngine before shipping.
  2. Don't add new injection-detection patterns only to DANGEROUS_PATTERNS without testing them against the combined prompt_content + system_message stringPromptSafetyScanner.scan is effective precisely because it lowers both fields into one combined string before calling re.search; a pattern scoped to only prompt_content misses identical injections smuggled through the system_message argument.
  3. Don't transition a prompt version to ACTIVE status without a PromptApproval record that has approved=True, a non-null reviewer, and every StageResult.passed equal to True — the registry update is the final write in the pipeline; updating status from REVIEW to ACTIVE on a partially-completed or human-rejected approval silently puts an untested or flagged prompt into live traffic with no audit trail of which stage it failed.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.

From · cancel anytime · Already a subscriber? Sign in →

More free lessons in AI Developer Platform Engineering

All free lessons in GenAI Platform Engineering