Engineer connecting prompt design with tools structured outputs and evaluation
AI Skills

Prompt Engineering Career Guide

Learn how to prove prompt-engineering ability with a fixed support-ticket evaluation, strict structured-output checks, failure analysis, and reproducible before-and-after evidence.

Aug 5, 202616 min readMuhammad FarooqLast reviewed: Aug 26, 2026

Prompt engineering is useful, but a screenshot of one polished response is weak evidence of engineering ability. Stronger proof starts with a measurable task, keeps a fixed evaluation fixture, validates outputs in deterministic code, classifies failures, and reruns the same cases after a controlled intervention. This guide demonstrates that workflow with synthetic recorded outputs for one support-ticket classifier. It is an educational fixture, not a provider benchmark, production system, or Farooq77 client result.

Prompt engineering begins with a measurable task

Define what success and safe failure mean before changing instructions.

The user outcome is a routing proposal that a support application can inspect: one controlled category, an escalation decision, and a short reason grounded in the ticket. The classifier does not send messages, alter accounts, issue refunds, or perform any irreversible action. Those permissions remain in deterministic application and business-rule code.

Acceptance criteria separate concerns that a single accuracy number would hide. An output must parse, match the exact contract, use a supported category, select the expected category, make the expected escalation decision, and explain that decision without inventing facts. Ambiguous, multi-intent, or unsupported requests take the explicit review path: category other with escalate true.

Prohibited behavior includes guessing missing account facts, inventing incidents or prior actions, producing extra fields, selecting a category outside the enum, or treating the model's escalation suggestion as authorization. Retrieval or context can also be wrong; adding stronger wording cannot repair missing or stale evidence.

Evaluation loop from measurable task and fixed cases through a prompt and context version, recorded output, schema checks, task graders, failure categories, and a controlled choice among prompt, context, schema, model, tool, or business-rule interventions before rerunning the same cases
Evaluation turns a prompt edit into one possible intervention inside a controlled loop. The same fixture and graders remain fixed while the diagnosed failure determines what should change.
Acceptance criteria for the synthetic classifier
CriterionDeterministic checkWhy it exists
Structured outputParse JSON; require exactly category, escalate, and reason with correct typesA routing service needs a stable boundary
Controlled categoryAllow only billing, account_access, technical_issue, feature_request, or otherValid JSON can still contain an unusable domain value
Task correctnessCompare category and escalation with fixed labelsSchema adherence does not prove the decision is correct
Safe uncertaintyRequire other plus escalation for marked review casesAmbiguous or unsupported work needs a human-owned path
Grounded reasonRequire fixture evidence terms and reject known invented detailsThe explanation must not add facts absent from the ticket

Worked example — support-ticket classification

One compact fixture makes the prompt, contract, cases, and recorded behavior inspectable.

Each input is a synthetic support ticket. The required result is {category, escalate, reason}. Billing covers charges and invoices; account_access covers sign-in and account-security access; technical_issue covers product errors; feature_request covers requested capabilities; other is the safe boundary for unsupported, ambiguous, or multi-intent requests.

The baseline is plausible but under-specified. It asks for JSON without listing every allowed value, defining escalation boundaries, forbidding extra fields, or explaining how to handle mixed intents. The fixture then exposes those omissions instead of relying on a favorable screenshot.

All eight cases and both output sets are recorded educational data. They let the evaluator demonstrate deterministic grading without an API key. The counts apply only to these hand-authored records; they are not measurements of OpenAI, another provider, production traffic, or general model accuracy.

Baseline prompt with intentionally incomplete boundariesprompts/baseline.txt
Classify the support ticket for the support team.
Return JSON with category, escalate, and reason.
Escalate urgent or unusual requests.
Keep the reason short.
Strict output contract enforced outside the modelevals/output-schema.json
{
  "type": "object",
  "additionalProperties": false,
  "required": ["category", "escalate", "reason"],
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "account_access", "technical_issue", "feature_request", "other"]
    },
    "escalate": {"type": "boolean"},
    "reason": {"type": "string", "minLength": 1, "maxLength": 160}
  }
}
Eight fixed synthetic cases used by both runsevals/cases.json
[
  {
    "id": "case-01",
    "ticket": "My card was charged twice for order 1042.",
    "expected": {"category": "billing", "escalate": false},
    "needsReview": false,
    "groundingTerms": ["charged twice", "order 1042"],
    "forbiddenTerms": ["refund issued"]
  },
  {
    "id": "case-02",
    "ticket": "I cannot sign in after several password resets, and an alert says a new device accessed my account.",
    "expected": {"category": "account_access", "escalate": true},
    "needsReview": false,
    "groundingTerms": ["cannot sign in", "new device", "password resets"],
    "forbiddenTerms": ["account was hacked"]
  },
  {
    "id": "case-03",
    "ticket": "The app crashes whenever I attach a PDF.",
    "expected": {"category": "technical_issue", "escalate": false},
    "needsReview": false,
    "groundingTerms": ["crashes", "attach a pdf", "pdf"],
    "forbiddenTerms": ["latest app update", "known outage"]
  },
  {
    "id": "case-04",
    "ticket": "Please add dark mode to the reporting screen.",
    "expected": {"category": "feature_request", "escalate": false},
    "needsReview": false,
    "groundingTerms": ["dark mode", "reporting screen"],
    "forbiddenTerms": ["scheduled for release"]
  },
  {
    "id": "case-05",
    "ticket": "I was charged, but the invoice has no order number and I cannot tell which account it belongs to.",
    "expected": {"category": "other", "escalate": true},
    "needsReview": true,
    "groundingTerms": ["charged", "no order number", "which account"],
    "forbiddenTerms": ["fraud confirmed"]
  },
  {
    "id": "case-06",
    "ticket": "Can you tell me when my parcel will arrive?",
    "expected": {"category": "other", "escalate": true},
    "needsReview": true,
    "groundingTerms": ["parcel", "arrive"],
    "forbiddenTerms": ["tracking shows"]
  },
  {
    "id": "case-07",
    "ticket": "The dashboard shows error E17 after I click Save.",
    "expected": {"category": "technical_issue", "escalate": false},
    "needsReview": false,
    "groundingTerms": ["error e17", "click save", "dashboard"],
    "forbiddenTerms": ["database corruption"]
  },
  {
    "id": "case-08",
    "ticket": "The app is slow, and I also need a copy of all data stored for my account.",
    "expected": {"category": "other", "escalate": true},
    "needsReview": true,
    "groundingTerms": ["app is slow", "copy of all data", "data"],
    "forbiddenTerms": ["export is ready"]
  }
]
Synthetic recorded baseline outputs — not provider resultsevals/recorded-baseline.json
[
  {"id": "case-01", "output": "{\"category\":\"billing\",\"escalate\":false,\"reason\":\"The card was charged twice for order 1042.\"}"},
  {"id": "case-02", "output": "{\"category\":\"account_access\",\"escalate\":false,\"reason\":\"The user cannot sign in after password resets.\"}"},
  {"id": "case-03", "output": "{\"category\":\"technical_issue\",\"escalate\":false,\"reason\":\"PDF uploads fail after the latest app update.\"}"},
  {"id": "case-04", "output": "{\"category\":\"product_feedback\",\"escalate\":false,\"reason\":\"The user requests dark mode.\"}"},
  {"id": "case-05", "output": "{\"category\":\"billing\",\"escalate\":false,\"reason\":\"The ticket mentions a charge with no order number.\"}"},
  {"id": "case-06", "output": "{\"category\":\"other\",\"escalate\":false,\"reason\":\"Parcel delivery is outside the supported categories.\"}"},
  {"id": "case-07", "output": "category=technical_issue; escalate=false; reason=error E17 on Save"},
  {"id": "case-08", "output": "{\"category\":\"other\",\"escalate\":true,\"reason\":\"The ticket combines a slow app with a request for account data.\"}"}
]
Synthetic recorded revised outputs — one regression remainsevals/recorded-revised.json
[
  {"id": "case-01", "output": "{\"category\":\"billing\",\"escalate\":false,\"reason\":\"The card was charged twice for order 1042.\"}"},
  {"id": "case-02", "output": "{\"category\":\"account_access\",\"escalate\":true,\"reason\":\"Sign-in failed after password resets and a new-device alert.\"}"},
  {"id": "case-03", "output": "{\"category\":\"technical_issue\",\"escalate\":false,\"reason\":\"The app crashes when a PDF is attached.\"}"},
  {"id": "case-04", "output": "{\"category\":\"feature_request\",\"escalate\":false,\"reason\":\"The ticket requests dark mode on the reporting screen.\"}"},
  {"id": "case-05", "output": "{\"category\":\"other\",\"escalate\":true,\"reason\":\"The invoice has no order number and the account is unclear.\"}"},
  {"id": "case-06", "output": "{\"category\":\"other\",\"escalate\":true,\"reason\":\"Parcel arrival is outside the supported ticket categories.\"}"},
  {"id": "case-07", "output": "{\"category\":\"technical_issue\",\"escalate\":false,\"reason\":\"The dashboard shows error E17 after Save is clicked.\"}"},
  {"id": "case-08", "output": "{\"category\":\"technical_issue\",\"escalate\":false,\"reason\":\"The app is slow.\"}"}
]

Diagnose baseline failures

Failure labels preserve the evidence needed to choose an intervention.

The baseline does not fail in one uniform way. case-07 is malformed and therefore invalid_schema. case-04 is more instructive: product_feedback is syntactically valid JSON and the fields have the right primitive types, but the category is outside the controlled enum. It is unsupported_category, proving that JSON validity, schema validity, and domain validity are different gates. The complete evaluator taxonomy is invalid_schema, unsupported_category, wrong_classification, missed_escalation, false_escalation, fabricated_or_unsupported_reason, and ambiguous_or_needs_review.

case-02 chooses the right category but misses an escalation signal. case-03 classifies correctly yet invents a latest-update detail. case-05 forces a mixed, incomplete billing/account request into billing, while case-06 recognizes an unsupported request but does not send it to review. A prompt screenshot would hide most of this behavior.

Deterministic baseline result for each fixed case
CaseResultFailure evidence
case-01PASSBilling decision, escalation, and reason meet the fixture
case-02FAIL missed_escalationNew-device and repeated-reset signals were not escalated
case-03FAIL fabricated_or_unsupported_reasonReason adds a latest-update claim absent from the ticket
case-04FAIL unsupported_categoryValid JSON contains product_feedback, which is outside the enum
case-05FAIL wrong_classification + missed_escalation + ambiguous_or_needs_reviewIncomplete account ownership should take the other/review path
case-06FAIL missed_escalation + ambiguous_or_needs_reviewUnsupported parcel request is categorized other but not reviewed
case-07FAIL invalid_schemaRecorded output is not JSON
case-08PASSMulti-intent request takes the other/review path

Improve the system, not just the wording

The revision combines clearer instructions with deterministic enforcement and a review boundary.

The revised prompt names the enum, boundary rules, exact keys, escalation conditions, and prohibition on invented facts. Examples could be added only after a measured boundary failure shows they help; more examples are not automatically better. The application still owns parsing, extra-field rejection, enum enforcement, reason length, and the final authorization to route or act.

Where a provider supports strict Structured Outputs, supply the schema through that mechanism instead of relying on prose alone. Application-side checks remain useful for incomplete responses, integration mistakes, business rules, and the review path. If the wrong answer comes from missing policy or account context, change retrieval or supplied context and rerun the same cases rather than adding unrelated prompt warnings.

The revision deliberately leaves case-08 wrong in the recorded outputs. Its technical symptom attracts the classifier even though the data-copy request makes the ticket multi-intent and review-worthy. That remaining regression is more credible evidence than a hand-edited perfect result.

Revised prompt with explicit task and uncertainty boundariesprompts/revised.txt
Classify one support ticket.

Allowed categories:
- billing: charges, payments, invoices
- account_access: sign-in, password, or account-security access
- technical_issue: product errors, crashes, or broken behavior
- feature_request: requested product capability
- other: unsupported, ambiguous, or multi-intent request

Return exactly one JSON object with only:
{"category":"<allowed value>","escalate":<boolean>,"reason":"<1-160 characters>"}

Rules:
- Use other and escalate=true when the request is unsupported, ambiguous, or spans categories without a safe single route.
- Escalate account-access tickets that mention suspected unauthorized access or repeated failed recovery.
- Do not invent facts, actions, diagnoses, policy, or account state.
- Ground reason only in the ticket.
- Classification never authorizes a refund, account change, deletion, disclosure, or other side effect.

Compare before and after

The evaluator changes output records, not the fixture or graders.

A small ESM runner reads cases.json plus each recorded output set. It parses the output, rejects missing or extra keys and wrong types, enforces the enum, compares expected category and escalation, checks the explicit review convention, and applies transparent grounding terms. These lexical grounding checks are intentionally narrow; a production evaluator would need calibrated semantic or human review for nuanced explanations.

Both runs use the same eight cases and the same code. The baseline passes 2/8; the revision passes 7/8. Schema-valid outputs rise from 6/8 to 8/8, but case-08 regresses from pass to wrong_classification, missed_escalation, and ambiguous_or_needs_review. These are fixture results, not model statistics.

Dependency-free deterministic evaluator for both recorded runsevals/run-eval.mjs
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const dir = path.dirname(fileURLToPath(import.meta.url));
const read = (name) => JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
const cases = read("cases.json");
const allowed = new Set(["billing", "account_access", "technical_issue", "feature_request", "other"]);
const required = ["category", "escalate", "reason"];

function inspect(raw) {
  let value;
  try { value = JSON.parse(raw); } catch {
    return { value: null, basicValid: false, failures: ["invalid_schema"] };
  }
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return { value, basicValid: false, failures: ["invalid_schema"] };
  }
  const keys = Object.keys(value).sort();
  const shapeValid = keys.length === required.length
    && required.every((key) => keys.includes(key))
    && typeof value.category === "string"
    && typeof value.escalate === "boolean"
    && typeof value.reason === "string"
    && value.reason.trim().length > 0
    && value.reason.length <= 160;
  if (!shapeValid) return { value, basicValid: false, failures: ["invalid_schema"] };
  return { value, basicValid: true, failures: allowed.has(value.category) ? [] : ["unsupported_category"] };
}

function grade(testCase, raw) {
  const inspected = inspect(raw);
  const output = inspected.value;
  const failures = [...inspected.failures];
  const categorySupported = inspected.basicValid && allowed.has(output.category);
  const classificationCorrect = categorySupported && output.category === testCase.expected.category;
  const escalationCorrect = inspected.basicValid && output.escalate === testCase.expected.escalate;
  const reviewCorrect = inspected.basicValid && (!testCase.needsReview || (output.category === "other" && output.escalate));
  const reason = inspected.basicValid ? output.reason.toLowerCase() : "";
  const reasonGrounded = inspected.basicValid
    && testCase.groundingTerms.some((term) => reason.includes(term.toLowerCase()))
    && !testCase.forbiddenTerms.some((term) => reason.includes(term.toLowerCase()));

  if (categorySupported && !classificationCorrect) failures.push("wrong_classification");
  if (inspected.basicValid && !escalationCorrect) failures.push(output.escalate ? "false_escalation" : "missed_escalation");
  if (inspected.basicValid && !reasonGrounded) failures.push("fabricated_or_unsupported_reason");
  if (inspected.basicValid && testCase.needsReview && !reviewCorrect) failures.push("ambiguous_or_needs_review");

  return { failures, metrics: { schemaValid: inspected.basicValid && categorySupported, classificationCorrect, escalationCorrect, reviewCorrect, reasonGrounded, pass: failures.length === 0 } };
}

function run(label, filename) {
  const recorded = read(filename);
  assert.equal(recorded.length, cases.length, `${label} output count differs from fixture`);
  const byId = new Map(recorded.map((item) => [item.id, item.output]));
  assert.equal(byId.size, cases.length, `${label} has duplicate or missing IDs`);
  const results = cases.map((testCase) => {
    assert(byId.has(testCase.id), `${label} is missing ${testCase.id}`);
    return { id: testCase.id, ...grade(testCase, byId.get(testCase.id)) };
  });
  console.log(label);
  for (const result of results) console.log(`${result.id} ${result.metrics.pass ? "PASS" : `FAIL ${result.failures.join("+")}`}`);
  const count = (key) => results.filter((result) => result.metrics[key]).length;
  console.log(`summary schema_valid=${count("schemaValid")}/${cases.length} classification_correct=${count("classificationCorrect")}/${cases.length} escalation_correct=${count("escalationCorrect")}/${cases.length} review_behavior_correct=${count("reviewCorrect")}/${cases.length} reason_grounded=${count("reasonGrounded")}/${cases.length} overall_pass=${count("pass")}/${cases.length}`);
}

run("baseline", "recorded-baseline.json");
run("revised", "recorded-revised.json");

Expected output

baseline
case-01 PASS
case-02 FAIL missed_escalation
case-03 FAIL fabricated_or_unsupported_reason
case-04 FAIL unsupported_category
case-05 FAIL wrong_classification+missed_escalation+ambiguous_or_needs_review
case-06 FAIL missed_escalation+ambiguous_or_needs_review
case-07 FAIL invalid_schema
case-08 PASS
summary schema_valid=6/8 classification_correct=5/8 escalation_correct=4/8 review_behavior_correct=5/8 reason_grounded=6/8 overall_pass=2/8
revised
case-01 PASS
case-02 PASS
case-03 PASS
case-04 PASS
case-05 PASS
case-06 PASS
case-07 PASS
case-08 FAIL wrong_classification+missed_escalation+ambiguous_or_needs_review
summary schema_valid=8/8 classification_correct=7/8 escalation_correct=7/8 review_behavior_correct=7/8 reason_grounded=8/8 overall_pass=7/8
Same fixture and graders; only the recorded system version changes
MetricBaselineRevisedInterpretation
Schema valid6/88/8Malformed and unsupported-enum outputs are removed
Classification correct5/87/8Boundary rules improve cases 04 and 05; case 08 regresses
Escalation correct4/87/8Urgent and review paths improve; case 08 remains missed
Review behavior correct5/87/8Ambiguous/unsupported cases improve except the multi-intent case
Reason grounded6/88/8Recorded reasons no longer add a known forbidden detail
Complete pass2/87/8Useful improvement, not a perfect or general benchmark

Why one aggregate score is insufficient

An overall 7/8 hides whether the remaining problem is formatting, routing, escalation, review handling, or explanation quality. Metric lanes make the next decision inspectable. Schema validity asks whether downstream code can accept the object. Classification and escalation ask whether the task decision matches the fixture. Review behavior asks whether uncertainty is surfaced. Grounding asks whether the reason stays within available evidence.

The small fixture is deliberately educational, not statistically representative. Before using a similar evaluator for a real workflow, expand cases from authorized domain data, calibrate labels with subject-matter reviewers, define sampling and change-control rules, and retain human review for judgments that lexical checks cannot settle.

  • Do not count lower latency or cost as a quality gain unless the existing task graders still pass.
  • Do not change labels or replace hard cases after seeing the revised output; add new cases as a separately reviewed fixture version.
  • Do not treat schema validity as permission to execute a side effect.
  • Inspect regressions per case even when the aggregate score improves.

Separate failure ownership

The correct fix depends on where the evidence says the defect lives.

Prompt ownership covers unclear task boundaries, missing category definitions, or instructions that conflict. It does not absorb every failure in an AI application. A model cannot recover context it never received, a schema cannot determine business authorization, and a prompt cannot make a failed provider or tool call succeed.

Authorization deserves a hard boundary. Even if a classifier says escalate=false or recommends a refund, account deletion, data disclosure, or message send, deterministic policy and permission checks must decide whether that action is allowed. Rephrasing the prompt is not an authorization control.

Failure-ownership decision table
Observed failurePrimary ownerControlled intervention
Boundary is undefined or instructions conflictPromptClarify outcome, enum, priority, or uncertainty rule; rerun the fixture
Needed policy or account fact was absent/staleContext or retrievalFix source selection, freshness, access, or context assembly
JSON parses but fields/types/enum are invalidSchema/applicationUse strict structured output where supported and reject invalid values in code
Same well-formed task repeatedly exceeds model capabilityModel/providerCompare a version or provider on the same fixture and graders
Lookup or external action failedTool/provider integrationFix arguments, timeout, retry, error handling, or tool contract
Output suggests an unauthorized or irreversible actionAuthorization/business-rule codeEnforce identity, permission, approval, and side-effect rules outside the prompt
Label or grader is ambiguousEvaluation designReview acceptance criteria and labels before interpreting the score

Turn the example into portfolio evidence

A portfolio reviewer should be able to reproduce what you claim. Keep the prompt versions, fixed case fixture, recorded outputs, output schema, evaluator, exact command, and failure report together. A short README should state that outputs are recorded, identify the Node version, explain every metric, and separate application tests from model-behavior evaluation.

If you want to implement the surrounding service, use the Python and API boundaries required by AI engineers. For presenting the evidence in a job search, continue with the AI engineer resume guide instead of turning this article into a project list.

  • README: task, non-goals, enum, review convention, metrics, limitations, and reproduction command.
  • Fixture: stable IDs, synthetic inputs, expected labels, grounding terms, and version history.
  • Evaluator: dependency-free validation, per-case failures, metric-lane summaries, and a nonzero exit on broken fixture structure where appropriate.
  • Failure report: baseline evidence, controlled intervention, before/after output, remaining regression, and proposed next test.
  • Version record: prompt, context/retrieval configuration, schema, provider/model identifier when a real run exists, and grader version.

Durable role directions

Prompt capability becomes more useful when combined with applied AI engineering, evaluation design, backend integration, automation, data/retrieval work, or product and operations understanding. The durable contribution is not ownership of clever wording; it is the ability to define behavior, build evidence, diagnose failures, and place uncertain model output inside controlled software boundaries.

For workflow and integration depth, follow the AI automation engineer learning path. Role titles and demand can change, so treat this combination as capability planning rather than a guaranteed career forecast.

FAQ

Are these results a benchmark of an OpenAI model?

No. Both output sets are explicitly synthetic recorded examples. The evaluator proves its own validation and grading behavior on eight fixed cases; it makes no claim about any provider, model, production workload, or general accuracy.

Does valid JSON mean the classifier output is safe to use?

No. JSON parsing checks syntax. Schema checks required fields and types. Domain checks enforce values such as the category enum. Task graders inspect correctness, and deterministic authorization code decides whether any action is permitted.

Should every failure lead to a prompt edit?

No. Change the prompt for instruction or boundary defects. Change context or retrieval for missing evidence, schema/application code for invalid contracts, provider or tool integration for execution failures, and authorization code for permissions and irreversible side effects.

What makes this stronger portfolio evidence than a prompt gallery?

The task, fixture, prompts, recorded outputs, schema, evaluator, exact results, failure categories, and remaining regression are inspectable. A reviewer can reproduce the comparison and challenge its limitations instead of trusting selected screenshots.

Sources

Primary and authoritative sources reviewed for this article.

Conclusion

Start by copying the seven canonical artifacts from this article into one small folder and run node evals/run-eval.mjs. Then add one new synthetic edge case that exposes a real boundary, label it before changing the prompt, and rerun both versions. That controlled loop demonstrates prompt-engineering judgment more clearly than another polished response screenshot.