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.
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.
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.
Generic instructions ("be accurate") do nothing. Name the failure:
never invent prices or specificationsuse only facts given; do not use outside knowledgequote must be verbatim, at most 25 wordsif no owner is named, write UNASSIGNED - never guess a nameEach of those maps to a real, observed way these systems fail.
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]
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.
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.
12 production prompt contracts with fixed JSON schemas, plus a batch runner that turns a spreadsheet into a spreadsheet.
Get it — 19 USDT