Prompt Engineering & Structured Output
Everything Domain 4 tests: explicit criteria over vague guidance, few-shot examples, tool_use with JSON schemas for guaranteed structure, nullable fields against hallucination, validation-retry loops and multi-pass review, and choosing between the real-time Messages API and the Message Batches API. Every concept comes with the real request shape or code — because that is how the exam frames its questions.
4.1 Explicit criteria beat vague guidance
The exam rewards prompts that state categorical, checkable criteriaover vague adjectives. "Review this code for quality issues" and "check this document for problems" sound reasonable, but they hand the model an ill-defined bar — every response invents its own definition of "quality" or "problem," and no two runs grade the same way.
Compare the two versions of a code-review prompt used at scale (500 PRs a week, every PR reviewed the same way):
| Vague (fails at scale) | Explicit criteria (scales) |
|---|---|
| "Review this code for quality issues." | "Check for exactly these categories: (1) SQL injection risk, (2) missing null checks, (3) unbounded loops, (4) hardcoded secrets. For each category, output {category, found: bool, line, detail}." |
| Output shape varies run to run; nothing to diff against | Fixed shape; two runs on the same PR are directly comparable |
| A human still has to read prose to find what was actually checked | A missing category is visible immediately — the output says so |
The distinction matters most exactly where the exam likes to test it: a prompt running at scale. Reviewing 1 PR, a vague prompt might work fine because a human reads the output and fills the gaps. Reviewing 500 PRs a week with no human reading every response, vague instructions produce inconsistent, unverifiable output — while explicit criteria produce reproducible, gradeable output that a downstream system (or a human doing spot-checks) can actually act on.
4.2 Few-shot examples: show what's hard to say
Some tasks have a desired output format or edge-case handling that is hard to state in words but easy to show. When that is true, 2-4 input/output example pairs placed in the prompt before the real input anchor the model's behavior far more reliably than a longer written description alone.
Support-ticket classification: description-only vs few-shot
A prompt that lists categories in prose — "classify as billing, technical, or account" — under-specifies exactly the tickets that matter most: the ambiguous ones.
# Description-only (under-specifies edge cases)
"Classify the following support ticket as one of: billing,
technical, or account. Respond with only the category name."
Ticket: "My payment method got reset when I changed my email."
-> unclear which category this should be; the model guesses# Few-shot (anchors the exact edge-case handling)
Ticket: "I was charged twice for my subscription this month."
Category: billing
Ticket: "The app crashes every time I try to upload a photo."
Category: technical
Ticket: "My payment method got reset when I changed my email
address, and now I can't tell if my subscription is still active."
Category: account
# (edge case: touches billing AND technical, but the root cause
# is an account-settings change - the example teaches the model
# to classify by ROOT CAUSE, not by which keyword appears)
Ticket: "{{real_ticket}}"
Category:The third example pair is doing the real work: it is deliberately an ambiguous ticket, and it shows the model exactly how to resolve the ambiguity (classify by root cause, not by surface keyword). No amount of additional prose describing the "account" category would teach that resolution rule as reliably as one worked example.
4.3 tool_use for guaranteed structure
Asking Claude to "please respond in JSON" in plain text is not reliable structured output — it is a request the model interprets like any other instruction, and nothing on the API side validates the response against a shape. The response can come back with extra prose around the JSON, a dropped comma, or schema drift on a field name.
Giving Claude a tool definition with a JSON input_schema— even when the "tool" isn't really an external action, just a way to force schema-shaped output — is far more reliable than asking for JSON in plain text. One precision worth knowing for the exam: plain tool_use alone is very reliable but does not strictly guarantee conformance — the model can still infer a value for a missing required field. Adding strict: true to the tool definition (Strict tool use) is what turns that into a real guarantee, validating the call arguments against the schema before you ever see them.
Tool schema for invoice-field extraction
{
"name": "record_invoice_fields",
"description": "Record the extracted fields from a vendor invoice.",
"input_schema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"vendor_name": { "type": "string" },
"amount": { "type": "number" },
"due_date": { "type": "string", "format": "date" },
"po_number": { "type": ["string", "null"] }
},
"required": ["invoice_number", "vendor_name", "amount", "due_date", "po_number"]
}
}Claude responds with a tool_use block whose input already matches this shape — no free text to strip, no brackets to hunt for. Your code reads tool_use.input.invoice_number directly.
input_schema (add strict: true for a hard guarantee, not just high reliability), or the structured-outputs output_config.formatfeature — never "emphasize the JSON instruction more strongly" or "lower the temperature." A prompt-only fix raises the probability of compliance; it never guarantees it.4.4 Nullable fields prevent hallucination
A schema-shaped answer solves one problem and creates another if you are not careful: if a schema marks a field as required but the source document simply does not contain that data, the model will often invent a plausible-looking value to satisfy the schema — for example, fabricating a PO number that looks right but does not exist on the invoice.
The schema-level fix has two parts, and both matter:
- Mark optional fields nullable in the schema itself —
"type": ["string", "null"]— so the model has a valid, schema-conformant way to say "this isn't here" instead of being forced to produce something. - Explicitly instruct the null path— "If the PO number is not printed on the invoice, return
null. Do not guess or infer a value." — and validate downstream: flag or reject any record where a nullable field is unexpectedly populated with a low-confidence-looking value, or route null-heavy records for human review.
This is a direct continuation of the structured-output problem in 4.3: getting a schema-shaped answer is necessary but not sufficient — the schema also has to make the true absence of data representable, or the model will paper over the gap.
4.5 Validation-retry loops and multi-pass review
Once you have structured output, the production pattern is to run it through a programmatic validator— a schema check, plus business-rule checks like "due_date must be after invoice_date." If validation fails, the architecture retries the extraction call with the validation error appended to the prompt, so the model can see exactly what was wrong and self-correct — up to a bounded retry count.
That bound is not optional. It is the same loop-bounding principle from Domain 1: an agentic loop that is allowed to retry forever is a production incident waiting to happen, whether the loop is calling tools or re-attempting a structured extraction. A typical bound is 3 attempts, after which the record is flagged for human review — never silently dropped, and never retried indefinitely.
def extract_with_validation(document, max_retries=3):
error_context = ""
for attempt in range(max_retries):
result = extract(document, prior_error=error_context)
errors = validate(result) # schema check + business rules
if not errors:
return result # accept
error_context = f"Previous attempt failed: {errors}. Fix these issues."
flag_for_human_review(document, result, errors) # bounded - never loop forever
return NoneMulti-pass review: a cheap draft, a stronger check
A related production pattern is multi-pass review: a cheap or fast model pass drafts the extraction or output, then a second pass — the same or a stronger model, reviewing against explicit criteria — checks the draft. This catches errors a single pass misses, at a cost premium that is worth paying for high-stakes output (financial figures, medical data, anything that feeds a downstream decision without further human review).
4.6 Message Batches API — when async wins
The Message Batches API lets you submit many independent requests together and get results back within a 24-hour processing window, at roughly 50% lower cost than real-time calls. It is built for large, deadline-tolerant, non-interactive workloads — the textbook case is a nightly extraction job over 20,000 archived documents, where nothing downstream is blocked on the job finishing before the next morning.
This maps directly onto Domain 1's anti-pattern #3: using the Batches API for a blocking, user-facing, real-time flow is wrongbecause it has no real-time SLA — a user waiting on a live chat response cannot wait up to 24 hours for the reply. The mistake runs in both directions and the exam tests both: routing a live conversation through Batches is the classic wrong answer, and looping the real-time API over 20,000 documents just because "it's simpler to code" throws away a real cost saving for no architectural benefit.
| Real-time Messages API | Message Batches API | |
|---|---|---|
| Latency | Immediate — the caller is waiting | Up to 24 hours (most complete faster) |
| Cost | Full per-token price | ~50% lower for the same tokens |
| Use when | Someone or something is waiting synchronously on the response | The workload is large, independent, and deadline-tolerant |
Checkpoint quiz — Domain 4
Exam-style: one scenario, one correct answer, three plausible mistakes. Click an option to lock in and see the explanation.
A CI pipeline runs Claude against every pull request with the instruction "review this code for quality issues," and the results are inconsistent and hard to grade across 500 PRs a week. What should replace the vague instruction?
A team extracts 6 fixed fields from vendor invoices at 500/day. They currently ask Claude to "please respond in valid JSON only" in plain text, then call JSON.parse on the response — which occasionally throws on extra prose or malformed output. What is the architecturally correct fix?
A support-ticket classification prompt lists categories in prose ("billing, technical, or account") but keeps misclassifying ambiguous tickets, like "my payment method got reset when I changed my email." What should the architect add?
An invoice-extraction pipeline marks po_number as nullable and instructs Claude to return null — never guess — when the PO number is missing from the invoice. A downstream validator also checks that due_date falls after invoice_date. When that check fails, the pipeline retries the extraction, appending the validation error to the prompt, up to a bounded number of attempts before flagging the record for human review. A teammate wants to drop that validation-retry step entirely, arguing the nullable field alone already prevents every extraction error. Why is the teammate wrong?
A multi-agent research system crawls 20,000 archived documents every night. The results need to become structured citation records before the next day's research runs start, but no user is waiting on the extraction step itself. The coordinator (the agent orchestrating the pipeline) proposes looping the real-time Messages API over all 20,000 documents instead, reasoning that it is simpler to code than setting up a separate batch job. Is that the right call?
Quick glossary
Every term below appears somewhere in this course. If a question uses a word you're unsure of, check here before re-reading the whole lesson.
Agentic loop
The repeating cycle behind any Claude-powered agent: send a request, check what Claude wants to do next (stop_reason), run a tool if asked, send the result back, repeat until Claude is done.
stop_reason
A field on Claude's response telling your code why it stopped talking — e.g. it wants to call a tool (tool_use), it finished (end_turn), or it ran out of room (max_tokens).
tool_use / tool_result
tool_use is Claude asking your code to run something (a search, a database lookup...). tool_result is your code's answer, sent back so Claude can continue.
Subagent / Agent tool
A helper Claude spawns to do one focused piece of work in its own separate memory space, then reports back a summary — so the main conversation doesn't get cluttered with the helper's scratch work.
Hooks (PreToolUse / PostToolUse)
Small scripts that run automatically around a tool call — one right before it (can block it entirely) and one right after (can react to the result). Used for rules that must always hold, not just usually hold.
MCP (Model Context Protocol)
A standard way to connect Claude to external systems — databases, internal tools, company APIs — through a shared plug-in format instead of custom one-off code every time.
MCP Tools vs Resources vs Prompts
The three things an MCP connection can offer: Tools are actions Claude decides to call itself; Resources are data the surrounding app decides to hand Claude; Prompts are ready-made instructions a person picks, like a menu option.
.mcp.json
A config file listing which external MCP connections a project uses — shared with the whole team via version control, with any secret keys kept out of it and read from environment variables instead.
CLAUDE.md
A plain-text file Claude reads automatically at the start of every session in a project — the place for standing instructions like coding conventions or how the codebase is organized.
.claude/rules/
Instructions that only switch on for certain files (e.g. only when editing test files), instead of loading into every single session the way CLAUDE.md does.
Agent Skill
A packaged set of instructions Claude loads by itself when a task matches, without anyone typing a command — think of it as an instruction manual Claude picks up automatically off the shelf.
allowed-tools vs disallowed-tools
Two different settings that sound alike: allowed-tools just skips the usual 'are you sure?' prompt for tools you've pre-approved — it doesn't block anything else. disallowed-tools is what actually removes a tool from what's available.
Plan mode
A mode where Claude describes what it's about to do and waits for a yes before touching any files — used for bigger or riskier changes, skipped for small obvious ones.
Message Batches API
A cheaper way to send Claude a large pile of requests when nobody is waiting on the answer right away — costs about half as much, but can take up to a day to come back.
Prompt caching
A way to avoid Claude re-reading the same large, unchanging block of text (like a system prompt or a codebase) on every single request — it's remembered for a while, which is both cheaper and faster.
Context window
The total amount of conversation and text Claude can 'see' at once for a given request. When a conversation runs long, older parts may need to be summarized or dropped to make room.
Hub-and-spoke (multi-agent)
A setup where one coordinator agent hands out tasks to several helper agents and collects their answers — the helpers never talk directly to each other, only to the coordinator.
Anti-pattern
A design choice that looks reasonable but causes real problems in practice — this course names seven specific ones the exam likes to hide inside wrong answers.