{"slug": "designing-a-parser-contract-for-ai-output-not-just-a-prompt", "title": "Designing a parser contract for AI output (not just a prompt)", "summary": "Anguardia's import pipeline for AI-generated prospect research uses a deterministic parser that never fails, instead returning warnings for malformed or unrecognized data. The parser accepts legacy markers and drops unknown fields or invalid dates with visible warnings, treating blank as better than plausible. This approach ensures that AI output is handled reliably without dead ends.", "body_md": "Most posts about getting structured data out of an LLM stop at the prompt: ask for JSON, maybe hand it a schema, done. That's necessary but not sufficient — the harder problem shows up on the other end, in the code that has to trust what came back. I hit this building the import pipeline for a CRM ([Anguardia](https://anguardia.com)) that reads AI-generated prospect research, and the parser ended up teaching me more than the prompt did.\n\nThe prompt asks for a fixed markdown shape — headings, a table, checkbox tasks:\n\n``` php\n<!-- anguardia-dossier v1 -->\n# Dossier: <Company Name>\n\n## Company\n- Industry: <industry>\n- Website: <url>\n- Location: <city or region>\n- Source: <cold | referral | inbound | research>\n\n## People\n| Name | Role | Email | Phone | LinkedIn |\n|------|------|-------|-------|----------|\n\n## Suggested tasks\n- [ ] <task title> | due: <YYYY-MM-DD, optional>\n\n## Suggested outreach\n<the first message, under 150 words>\n```\n\nThat's the easy 80%. Any capable model follows a structure like this reliably. The interesting decisions all live in the parser that reads it back.\n\n```\n/** Deterministic Dossier v1 parse. Always returns a dossier object + warnings. */\nexport function parseDossier(text: string): ParseResult {\n  const warnings: string[] = [];\n  const hasMarker = textContainsDossierMarker(content);\n\n  if (!hasMarker) {\n    warnings.push(\"Dossier marker not detected. Parsing best-effort.\");\n  }\n  // ... parsing continues regardless\n```\n\nparseDossier has no failure mode — it always returns a dossier object and a warnings array, even for input that doesn't look like a dossier at all. A model's output is not a contract you control, so treating a malformed dossier as an error case just means building a second, worse UI for \"sorry, try again.\" Best-effort parsing plus visible warnings does the same job without the dead end.\n\n```\nif (!KNOWN_COMPANY_KEYS.has(key)) {\n  warnings.push(`Unknown company field ignored: ${bullet[1].trim()}`);\n  continue;\n}\n```\n\nA model will occasionally add a field nobody asked for, or misspell one. Silently coercing it into the nearest known field is how you end up with a company's Slack handle stored as its website. The parser drops anything it doesn't recognize and says so — a warning the user can see, not a guess they can't.\n\nSame logic on malformed data that did land in the right field:\n\n```\nif (DATE_RE.test(dueRaw)) {\n  dueDate = dueRaw;\n} else {\n  warnings.push(`Malformed due date ignored: ${dueRaw}`);\n}\n```\n\nA due date that isn't YYYY-MM-DD doesn't get parsed loosely — it gets dropped, with a warning. The alternative (a fuzzy date parser trying to make sense of whatever the model wrote) fails in a way nobody notices until a task has the wrong due date silently.\n\nThe prompt tells the model not to guess contact details:\n\n```\nNever invent. Leave any field blank if you cannot verify it from a real source.\nDo not guess emails, phone numbers, or names. Blank is always better than plausible.\n```\n\nThat's necessary but it's a request, not a guarantee — nothing stops a model from ignoring it. So the parser is built the same way independently: a blank table cell stays null, not an empty string coerced into something that looks like data. Two independent layers agreeing \"blank beats plausible\" is worth more than either one alone.\n\n``` php\nexport const DOSSIER_MARKER_LEGACY = \"<!-- founder-os-dossier v1 -->\";\nexport const DOSSIER_MARKER = \"<!-- anguardia-dossier v1 -->\";\nexport const DOSSIER_MARKERS = [DOSSIER_MARKER, DOSSIER_MARKER_LEGACY] as const;\n```\n\nThe product's name changed after the format shipped. Rather than migrate every dossier anyone had already generated, the parser just accepts both markers indefinitely. A one-line HTML comment on the first line is a cheap, durable version tag — cheaper than a schema registry, and it survives a rebrand without anyone having to regenerate old research.\n\nIf you're parsing anything an LLM produces and acting on it automatically:\n\nNever throw on malformed input — return a result plus diagnostics, always\n\nReject and report unknown data, don't coerce it — a warning is recoverable, a wrong guess isn't\n\nMatch your prompt's honesty constraints in the parser — don't rely on the model alone to keep a promise\n\nVersion the format at the boundary (a marker, a header), not by trying to migrate every past output\n\nNone of this is specific to CRMs or prospect research — it's the same shape for any pipeline where a model's output becomes a record something else acts on. The prompt gets the output roughly right most of the time. The parser is what makes \"most of the time\" safe to automate.\n\nIf you want to see the actual prompt this parses: [the free Dossier v1 prompt](https://anguardia.com/prospect-research-prompt) — paste it into Claude, ChatGPT, or any model with a company name, no signup required.", "url": "https://wpnews.pro/news/designing-a-parser-contract-for-ai-output-not-just-a-prompt", "canonical_source": "https://dev.to/anguardia/designing-a-parser-contract-for-ai-output-not-just-a-prompt-8gd", "published_at": "2026-08-10 12:41:41+00:00", "updated_at": "2026-08-10 12:48:57.651432+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "developer-tools"], "entities": ["Anguardia"], "alternates": {"html": "https://wpnews.pro/news/designing-a-parser-contract-for-ai-output-not-just-a-prompt", "markdown": "https://wpnews.pro/news/designing-a-parser-contract-for-ai-output-not-just-a-prompt.md", "text": "https://wpnews.pro/news/designing-a-parser-contract-for-ai-output-not-just-a-prompt.txt", "jsonld": "https://wpnews.pro/news/designing-a-parser-contract-for-ai-output-not-just-a-prompt.jsonld"}}