If the output shape can vary, you have not automated anything

Why every production prompt needs a fixed JSON schema and a field for 'I don't know' — plus the two-line check that catches invented facts.

The moment an LLM's output feeds a spreadsheet column, a database row, or another API call, output shape becomes a contract. A prompt that returns "a nice summary" returns a different shape on Tuesday and the thing downstream breaks silently.

Rule 1 — the schema goes in the prompt, and it is exact

Return exactly:
{"intent":"","urgency":"","key_asks":[""],"draft_reply":"","needs_human":true,"missing_info":""}

Not "return JSON with fields like intent and urgency". A literal skeleton, keys in order, types shown by example. This single change removes most parse failures.

Rule 2 — every schema needs a place to say "I don't know"

This is the part people skip, and it is the part that stops invented facts. Without a field for uncertainty, a model facing a missing value fills the hole with something plausible.

{"missing_info":""}          ' what stopped a complete answer
{"gaps":[""]}                ' what the source did not cover
{"cannot_assess":[""]}       ' which requirements the profile never addressed
{"missing_facts":[""]}       ' which claim would have had to be invented

Then check it: if missing_info is non-empty and draft_reply still asserts facts, the model ignored the rule. That is your defect signal, and it is measurable.

Rule 3 — forbid the specific failure you actually hit

Generic instructions ("be accurate") do nothing. Name the failure:

Each of those maps to a real, observed way these systems fail.

Rule 4 — make it verifiable, not just structured

Structure alone does not stop hallucination. Require a quote:

{"point":"","quote":"","confidence":"high|medium|low"}

Then verify mechanically — search each quote in the source. Not found means the model made it up. Two lines of code, and it is a real test rather than a vibe check.

for p in json.loads(out)["points"]:
    assert p["quote"] in source, "hallucinated: " + p["quote"][:40]

Rule 5 — failures stay in the output

When you batch 500 rows and 12 fail, a runner that silently drops them gives you 488 rows and false confidence. Keep the row, keep the error text:

{"row":13,"input":"...","output":"","error":"HTTP 429 after 3 retries"}

A batch that hides its failures is worse than no batch, because it looks finished.

Where this is packaged

Prompt Pack ships 12 contracts written this way (inbox triage, meeting actions, document briefs with quotes, contract risk scan, code review, data cleaning rules, column semantics, SOP writing) plus prompt_runner.py: spreadsheet in, spreadsheet out, concurrency, retries, failed rows preserved. 19 USDT, and --dry-run shows exactly what would be sent before you spend a token.

Prompt Pack

12 production prompt contracts with fixed JSON schemas, plus a batch runner that turns a spreadsheet into a spreadsheet.

Get it — 19 USDT