P3.6 · D2 + D5 · Process33% of CCA-F22 min build★ Official scenario 6 of 6

Structured Data Extraction.

A schema-driven extraction agent. The harness defines the output shape as a tool with input_schema, sets tool_choice to force that tool, runs a validation-retry loop (parse → validate → on failure feed the error back), uses nullable fields and enum escape hatches so the model says 'unclear' instead of fabricating, gates sensitive bounds with a PreToolUse hook (refund_amount > cap deny), and caches the schema with cache_control: ephemeral for ~90% cost reduction on bulk runs. The most-tested distractor: prompting 'output JSON' instead of forced tool_use. The former leaks 15%, the latter is a structural guarantee.

Mental modelArchitecture chooses the flow. Deterministic controls enforce policy. Structured state preserves truth.
Loop mascot illustrating Structured Data Extraction.
Share
01 · System & parts

What this system is, and its 5 parts

Think of this as the way you turn a messy email or a 200-page contract into a clean spreadsheet row, reliably, every time. Instead of asking the model to 'output JSON' and hoping (which fails about 15% of the time in production), you define exactly the shape you want as a tool schema, force the model to use that tool, and then validate every record before accepting it. When something is missing, you tell the model nullable fields are okay so it doesn't make data up. When the answer comes back wrong, the harness feeds the specific error back and asks again. The whole point is that extraction at scale needs deterministic shape AND honest gaps, not creative writing.

5Components
D2Primary domain
8Build steps
9Decision traps
8Concept links
Stack · Claude SDK · JSON Schema validator · Batch API for bulkNeeds · tool_use · tool_choice · prompt caching
Structured Data Extraction component architecture.
5 components. Each owns one concept.
01

JSON Schema Definition

the contract, in input_schema

The output shape lives inside a tool definition, not as freeform text instruction. Required vs nullable, enum vs string, integer vs number. Every property is explicit. The model can only emit a tool_use call that matches; the SDK rejects anything else.

Configurationtools = [{ name: "extract_record", input_schema: { type: "object", properties: { customer_id: { type: "string", pattern: "^cust_[0-9]+$" }, refund_amount: { type: ["number", "null"] }, refund_reason: { type: "string", enum: ["damage", "wrong_item", "late", "other", "unclear"] } }, required: ["customer_id", "refund_amount", "refund_reason"] } }]
Concept: structured-outputs
02

Forced tool_choice

tool_choice: { type: 'tool', name: ... }

Setting tool_choice to a specific tool name guarantees the model fires that tool. No prose wrapping, no 'I'd be happy to help' preamble, no probabilistic adherence. This is the single biggest reliability lever; it converts 85% prompt-only adherence into 100% structural adherence.

Configurationtool_choice: { type: 'tool', name: 'extract_record' }. Use 'auto' only for open-ended flows where the model decides whether to call any tool. Forced is for mandatory extraction.
Concept: tool-choice
03

Validation-Retry Loop

parse → validate → feed-error-back

Schema enforcement guarantees STRUCTURE. Semantic validation (date format, amount sign, ID pattern, business rules) runs in code after parse. On failure, the harness feeds a specific error message back to the model ('refund_amount is -50, must be ≥ 0') and retries. Typically converges in ≤ 2 retries. Generic 'try again' doesn't work; specific errors do.

Configurationloop: extract → parse → validate_semantically → if invalid, append { role: 'user', content: 'Validation failed: <specific error>. Re-extract.' } → retry. Max retries: 3. After 3, route to human review.
Concept: evaluation
04

Nullable Fields + Enum Escape Hatches

the anti-fabrication architecture

When a source genuinely doesn't contain a value, the model has two honest options: emit null (if the schema allows nullable) or emit a designated 'unclear' / 'not_provided' enum value. Without these escape hatches, required-string fields force the model to invent. Fabrication rate climbs above 5%. With them, fabrication drops below 1%.

ConfigurationField types: ["string", "null"] for optional values. Enums always include "unclear" or "other" as the last option. Few-shot examples explicitly show the model emitting unclear when source is silent. Anchors the behaviour.
Concept: structured-outputs
05

Schema Caching + Batch API

cost discipline at volume

The schema is the largest stable token cost (~500-2000 tokens depending on complexity). Mark the tools array with cache_control: ephemeral; the 5-min TTL keeps it warm across sustained traffic, dropping schema-token cost ~90%. For overnight bulk runs, the Batch API gives a flat 50% discount. Combined with caching, bulk extraction cost drops 95%+ vs naive sync calls.

ConfigurationSync API: tools array with cache_control: { type: 'ephemeral' }. Cache hit rate ≥ 70% within 5-min windows. Batch API: submit 1000+ extractions overnight, results within 24h, no real-time retries (resubmit failures the next batch).
Concept: prompt-caching
02 · Problem framing

The problem

What the user needs
  1. Guaranteed structure on every output. A downstream pipeline must never see a record missing a field.
  2. Honest non-answers when source data is genuinely missing. Better an explicit 'unclear' than a fabricated value.
  3. Bulk extraction at acceptable cost. 1000 documents/night at <$5 total.
Why naive approaches fail
  1. Prompt 'output JSON' → 15% leak with prose wrapping ('Sure, here's the JSON:'); downstream parser breaks.
  2. Required fields with no nullable option → model fabricates values when source is silent (refund_reason becomes 'customer dissatisfied' even when the email said nothing).
  3. Single-pass extraction with no retry → semantic errors slip through (date as 'next Tuesday', amount as -50, customer_id with embedded whitespace).
Definition of done
  • Schema conformance = 100% (forced tool_use guarantees shape)
  • Fabrication rate < 1% (nullable + enum escapes give the model an honest opt-out)
  • Validation-retry convergence ≥ 95% within 3 attempts; remainder routed to human review
  • Bulk runs use Batch API (50% discount, 24h SLA) for non-blocking volume
  • Schema cached with cache_control: ephemeral for ~90% savings on sustained traffic
03 · Data flow

One run, traced end to end

Structured Data Extraction sequence diagram.
Structured Data Extraction end-to-end flow.
04 · Build

8 steps to production

01

Author the JSON schema as a tool definition

Define the output shape in tools[0].input_schema. A JSON Schema object. Every required field listed in required[]. Every optional field has ["<type>", "null"] so the model can emit null. Every constrained string is an enum with an explicit escape (unclear, not_provided, other). Add pattern regex on IDs that have a known format.

Concept: structured-outputs
Python
from anthropic import Anthropic
client = Anthropic()

EXTRACT_TOOL = {
    "name": "extract_record",
    "description": "Extract a structured record from a customer email.",
    "input_schema": {
        "type": "object",
        "properties": {
            "customer_id": {"type": "string", "pattern": "^cust_[0-9]+$"},
            "refund_amount": {"type": ["number", "null"]},
            "refund_reason": {
                "type": "string",
                "enum": ["damage", "wrong_item", "late", "other", "unclear"],
            },
            "urgency": {
                "type": "string",
                "enum": ["low", "medium", "high", "unclear"],
            },
        },
        "required": ["customer_id", "refund_amount", "refund_reason", "urgency"],
    },
}
02

Force tool_choice to the extraction tool

tool_choice: { type: 'tool', name: 'extract_record' } is the structural contract. The model has no choice but to fire the tool with arguments matching the schema. Any prose preamble or wrapping disappears. This single setting turns 85% prompt-only adherence into 100% structural adherence.

Concept: tool-choice
03

Add nullable types and enum escape hatches

Every field that might be genuinely missing in the source gets ["<type>", "null"]. Every constrained string includes an explicit unclear / not_provided / other option. This gives the model an honest exit when the source is silent. Without it, required-string fields force fabrication. Pair with a few-shot example showing the model correctly emitting unclear.

Concept: structured-outputs
04

Wrap extraction in a validation-retry loop

Schema guarantees structure; semantics need code. After parsing the tool_use input, validate semantically: refund_amount > 0, customer_id matches the canonical pattern beyond the regex, urgency-vs-amount sanity (a $50K refund tagged 'low' is suspicious). On failure, feed a specific error back to the model and retry. Specific errors converge; generic 'try again' loops forever.

Concept: evaluation
05

Cache the schema with cache_control: ephemeral

The schema is the largest stable token cost in steady-state extraction (~500-2000 tokens for non-trivial shapes). Mark the tools array with cache_control: { type: 'ephemeral' }. The 5-min TTL keeps it warm across sustained traffic; cached input tokens cost ~10% of fresh tokens. Hit rate stays ≥ 70% with continuous extraction; ~90% schema-token savings.

Concept: prompt-caching
06

Add a PreToolUse hook on policy bounds

When extraction touches policy-bearing values (refund cap, transaction limits), don't trust the model. Wrap it in a PreToolUse hook that exits 2 on violation. The hook reads tool_input.refund_amount and compares to the known cap; on breach, it returns an error to the model with the cap reference, and the model re-extracts with the constraint visible. Deterministic policy enforcement, not probabilistic.

Concept: hooks
07

Use Batch API for bulk overnight runs

Sync API is the right call when latency matters. For overnight backfills (1000+ documents), the Batch API gives a flat 50% discount with a 24h SLA. Combined with schema caching, bulk extraction cost drops 95%+ vs naive sync calls. Resubmit failures as a new batch the next morning. Batch API is async, no real-time retry inside the batch.

Concept: batch-api
08

Stratified accuracy reporting (not aggregate)

A 95% aggregate accuracy can hide a 60% accuracy on a critical document type. Track validation pass rate stratified by source. By document type (email vs PDF vs HTML), by sender domain, by extraction date. Bad strata surface fast. Aggregate metrics lie; stratified ones tell the truth.

Concept: evaluation
05 · Right call, wrong call

9 decisions the exam turns into distractors

01 · Output shape guarantee
Looks right

Prompt instruction 'output JSON' or 'respond with valid JSON'

Actually right

Forced tool_choice with input_schema as the contract

DEC-01
02 · Field that might be missing in the source
Looks right

Required field with no nullable / no escape. Force the model to invent

Actually right

["<type>", "null"] AND/OR enum with explicit 'unclear' option

DEC-02
03 · Validation failure
Looks right

Generic 'please try again' or single-pass with no retry

Actually right

Validation-retry loop with the SPECIFIC error fed back

DEC-03
04 · 1000 extractions overnight
Looks right

Sync API with caching, or sync API without caching

Actually right

Batch API + schema caching (50% × ~90% = ~95% savings)

DEC-04
05 · Prompt-only JSON
Looks right

System prompt says 'respond with JSON only'. ~15% of responses include prose wrapping ('Sure, here's the JSON:'); downstream parser breaks on every fifth document.

Actually right

Forced tool_choice + input_schema. The model has no choice but to fire the tool with arguments matching the schema. 100% structural adherence.

AP-SDE-01
06 · Non-nullable required fields
Looks right

refund_reason is required string. Source email says nothing about a reason. Model fabricates 'customer dissatisfied' to satisfy the schema. Fabrication rate ~7%.

Actually right

Make the field nullable AND/OR add an explicit 'unclear' enum option. Few-shot one example showing the model emit 'unclear' on a silent source. Fabrication drops below 1%.

AP-SDE-02
07 · Single-pass extraction
Looks right

Validation fails (refund_amount = -50). The pipeline drops the record entirely. Operator sees 5% silent loss; nobody notices for two weeks.

Actually right

Validation-retry loop: feed the specific error back to the model, retry up to 3 times. 70-80% of validation failures converge within 2 retries.

AP-SDE-03
08 · Schema-only validation
Looks right

Schema accepts refund_amount: -50 (number type passes). Schema accepts customer_id: 'cust_ 42' (string type passes). Bad data ships downstream.

Actually right

Validate semantically AFTER schema parse: bounds checks (amount ≥ 0), regex on IDs (no embedded whitespace), business rules (urgency-vs-amount sanity). Schema enforces shape; code enforces meaning.

AP-SDE-04
09 · No policy hook on sensitive bounds
Looks right

Refund cap is enforced in the system prompt ('never extract refund_amount > 500'). Production sees 3% violations leak through; auditor flags.

Actually right

PreToolUse hook on extract_record: reads tool_input.refund_amount, compares to policy cap, exits 2 on breach with a specific error message. Deterministic, not probabilistic.

AP-SDE-05
Structured Data Extraction failure map.
06 · Budget

Cost & latency

~$0.0008-0.002Per-extraction (sync, cached schema)

Schema ~1500 tokens at cache-read price (~$0.0001) + email body ~500 input tokens + ~150 output tokens. Sustained traffic with ≥70% cache hit rate keeps per-record cost predictable.

~+30% on records that retryValidation-retry overhead

5-10% of records retry once; 1-2% retry twice. Specific-error feedback converges quickly. Overall pipeline cost up ~5% to gain ~99% schema-conformance + ~99% semantic-conformance.

~50% off sync, ~95% off naive uncached syncBulk overnight (Batch API)

Batch API flat 50% discount × schema caching ~90% savings = ~95% total. 1000 documents @ ~$0.0008 each (sync cached) drops to ~$0.0004 each (batch + cache). $0.40 vs $0.80 per 1000.

~0% token cost; ~50ms latencyHook overhead

PreToolUse hook is a Python/TS subprocess reading stdin and exiting 0/2. No LLM call. Cost is purely syscall-level latency.

~$0.80-2.00Per-1000-docs total (steady state, sync + cached)

Real production extraction at typical complexity. Batch + cache halves it. Adding human review of unconverged records adds operator-time cost but recovers the long tail.

07 · Ship checklist

Check every gate before release

0/11 checked
  • structured-outputs
  • tool-choice
  • structured-outputs
  • evaluation
  • prompt-caching
  • hooks
  • batch-api
08 · Practice

5 exam-pattern questions

Work through one question at a time, check the architecture, then move through the set.

Question 1 of 5 · D5Choose the best answer

A support conversation hits turn 30 with progressive summarization enabled, and the agent suddenly cannot recall the customer ID. What is missing from the architecture?

09 · FAQ

Frequently asked

Why not just prompt 'output JSON'. Claude is good at it now?

Probabilistic ≠ guaranteed. Even at 95% adherence, a 5% prose-wrapped output rate is one broken record per 20 documents. Forced tool_choice is structural. The SDK rejects anything that doesn't match the schema. The cost is identical; the reliability difference is decisive. Use prompts for tone; use forced tools for shape.

Can I cache the schema if it changes between calls?

Cache the stable parts. If the schema body is fixed but a few enum values vary by tenant, split the tools array: stable common schema (cached) + small per-tenant additions (fresh). Cache only what's stable across calls; the cache key is sensitive to byte-level changes.

How does Batch API interact with the validation-retry loop?

Batch is async. No inside-the-batch retry. Submit, wait 24h, harvest results. Validate each; if some fail, submit those failures (with the specific error in the next message) as a NEW batch. Most converge in batch-2. For records that need real-time retry, route them to the sync pipeline.

What's the difference between the schema's pattern regex and code-level validation?

Schema validates shape; code validates meaning. pattern: '^cust_[0-9]+$' rejects malformed IDs at parse time. Faster, structural. Semantic checks (the customer_id must exist in our DB, the refund_amount must be ≤ the original purchase total) need code; they're business rules, not syntax. Use pattern for cheap structural rejection; use code for everything that requires lookups or business logic.

Should I run extended_thinking with structured extraction?

Generally no. They're incompatible with forced tool_choice. When the model needs to reason about ambiguous source text, set tool_choice: 'auto' and accept ~95% reliability, OR run a sync pre-pass to disambiguate, then a forced extraction on the cleaned input. Don't try to combine extended_thinking with forced tool_choice in one call.

How do I test that nullable + enum escapes are working?

Adversarial test set. Compose 50 short inputs where the value is GENUINELY missing or ambiguous. Run extraction. The right behaviour is a mix of null and 'unclear'. Never invented values. If you see invented values (e.g. a refund_reason that's not in the source), the few-shot or the schema doesn't yet give the model an honest exit. Iterate.

Does forced tool_choice work with multi-tool registries?

Yes. Tool_choice picks one specific tool by name. Even in a 5-tool registry, tool_choice: { type: 'tool', name: 'extract_record' } forces THAT tool. The other 4 are inert for this call. Use this when extraction is mandatory but the broader agent has other tools available; for extraction-only, the registry can have just one tool.

When do I use Batch API vs sync vs caching for cost optimization?

Three-axis decision. (1) Latency-sensitive (interactive review, real-time agent loops): sync API + prompt-cache on the schema and stable system prompt. ~80% off the cached portion at >=70% hit rate. (2) Latency-tolerant bulk (>=100 docs overnight): Batch API for the flat 50% discount, 24h SLA. Combine with caching only at the per-batch sub-batch level (ephemeral cache is per-request in Batch). (3) Mixed traffic: sync the long tail, batch the predictable backlog. Cross-references: P3.5 uses Batch API for nightly audits; P3.8 uses Batch API for bulk long-doc extraction; P3.7 covers the 4-line tool-description pattern that keeps cached tools array stable. Tagged related: cost-optimization cluster.

Why does the model's self-reported confidence not gate routing?

Calibration anti-pattern. Models emit confidence scores that correlate weakly with actual correctness, especially on out-of-distribution inputs. A 0.95 confidence on a fabricated value still ships fabricated data. The right architecture is structural: validation-retry on schema + semantic checks (sum equals total, currency in enum, date sanity), and stratified accuracy reporting by document type. Use confidence as a soft signal in routing decisions (escalate to human if confidence < 0.5) but never as the primary gate. Tagged related: evaluation-and-evals cluster.

Help someone build it

Share this scenario.

One share is one less team repeating the same architecture mistake.