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
PromptEvalGatethat renders a prompt template against an evaluation dataset, collects float scores from the eval runner, and compares the average to a configurablethreshold(default0.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 floatscore, and a human-readabledetailsstring; the orchestrator appends oneStageResultper stage to thePromptApprovalas the pipeline runs. - PromptApproval — The accumulating record for an entire approval run, holding all
StageResultobjects across every stage plus the final reviewer decision (reviewer,reviewer_comment, andapproved); it is the artifact the prompt registry reads to determine whether to transition a version from REVIEW to ACTIVE. - Safety Scanner — The
PromptSafetyScannerclass that tests the combined prompt and system message against a list of regex patterns—such asignore previous instructionsand deeply nested variable expressions—to detect prompt injection attempts; any single match fails theSAFETY_CHECKstage outright. - Eval Runner — A duck-typed dependency injected into
PromptEvalGatethat 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).
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
- ✓Do set
PromptEvalGate.thresholdexplicitly for your dataset — the default of0.8is a starting point, not a contract; calibrating it against your evaluation dataset before wiring the gate into the pipeline ensures that theStageResult.passedverdict actually reflects acceptable quality, rather than an arbitrary number that silently passes degraded prompts. - ✓Do pass both
prompt_contentandsystem_messageintoPromptSafetyScanner.scan— the scanner concatenates both fields into a singlecombinedstring 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. - ✓Do return early from the orchestrator on the first
StageResultwherepassed=False— halting beforeSAFETY_CHECK,COST_REVIEW, orHUMAN_REVIEWwhen the eval gate fails keeps the reviewer queue free of prompts that didn't clear automated checks, and prevents a failingPromptApprovalrecord from accumulating misleading partial-pass stage results.
Don'ts
- ✗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 properInterpolationEnginebefore shipping. - ✗Don't add new injection-detection patterns only to
DANGEROUS_PATTERNSwithout testing them against the combinedprompt_content + system_messagestring —PromptSafetyScanner.scanis effective precisely because it lowers both fields into onecombinedstring before callingre.search; a pattern scoped to onlyprompt_contentmisses identical injections smuggled through thesystem_messageargument. - ✗Don't transition a prompt version to
ACTIVEstatus without aPromptApprovalrecord that hasapproved=True, a non-nullreviewer, and everyStageResult.passedequal toTrue— the registry update is the final write in the pipeline; updating status fromREVIEWtoACTIVEon 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
- Ch 12Design tool registry model with MCP server metadata
- Ch 12Deploy MCP hub with Helm and agent integration
- Ch 13Deploy managed pgvector with Helm StatefulSet
- Ch 14Deploy evaluation platform with Helm and Grafana
- Ch 15Build prompt approval workflow with evaluation gateYou are here
- Ch 16Deploy SLA monitoring with Grafana dashboards
- Ch 18Deploy change management with ArgoCD hooks