How to produce predictable, parseable and production-safe AI outputs with structured XML prompts

At myos we build AI operating systems for real businesses. The difference between a pretty demo and a robust production workflow almost always comes down to one place: the structure of the prompt. XML prompting is one of the most reliable methods for making model outputs predictable, machine-readable and safe to wire into downstream code.
This guide is our working reference. We reach for it when we design extraction pipelines, routing layers and structured generation. You can adopt the templates directly or adapt the tag names to your domain.
XML is particularly well suited for prompts for several reasons:
Use XML to enforce outputs, extract information, route tasks, enforce formats, mark up few-shot examples and run self-checks. And all of that without requiring hidden step-by-step reasoning (chain-of-thought).
Rule of thumb: XML or JSON?
If your prompts or outputs contain a lot of free text or code, XML plays to its strengths. For purely numerical, flat data structures, JSON can be the more compact choice.
<code><![CDATA[
def add(a, b): return a + b
]]></code>Start every new prompt with this skeleton. It contains metadata, task, instructions, input, output format and an error handler.
<prompt>
<meta version="1.0" id="GEN-001"/>
<task>Fasse einen langen Artikel für eine vielbeschäftigte Führungskraft zusammen.</task>
<instructions>
- Schreibe in einfacher Sprache.
- Halte dich an 120 bis 150 Woerter.
- Ergaenze 3 stichpunktartige Highlights.
</instructions>
<input>
<article><![CDATA[
{{ARTIKEL HIER EINFUEGEN}}
]]></article>
</input>
<output_format>
<summary>120 bis 150 Woerter.</summary>
<highlights>
<bullet>Praegnante Aussage</bullet>
<bullet>Praegnante Aussage</bullet>
<bullet>Praegnante Aussage</bullet>
</highlights>
</output_format>
<error_handler>
Wenn die Eingabe fehlt oder nicht auswertbar ist, gib zurueck:
<error code="INPUT_INVALID" message="Grund hier"/>
</error_handler>
</prompt>All templates deliberately avoid hidden chain-of-thought. Adopt them as-is or adapt the tag names to your domain.
<prompt>
<task>Extrahiere Firma, Produkt, Preis und Stimmung aus einer Rezension.</task>
<input>
<review><![CDATA[
Ich liebe den Acme Air 2000, nur 249 Dollar und leiser als mein alter Ventilator.
]]></review>
</input>
<output_format>
<extraction>
<company>string</company>
<product>string</product>
<price currency="USD">number</price>
<sentiment one_of="positive|neutral|negative">string</sentiment>
<key_points max="3">stichpunktartige Phrasen</key_points>
</extraction>
</output_format>
<instructions>
- Ist ein Feld unbekannt, setze ein leeres Tag (z. B. <price/>).
- Halte <key_points> auf hoechstens 3 kurze Stichpunkte.
</instructions>
</prompt><prompt>
<task>Klassifiziere die Absicht eines Support-Tickets.</task>
<labels>
<label id="bug">Fehlermeldung</label>
<label id="billing">Abrechnung</label>
<label id="feature">Feature-Wunsch</label>
<label id="other">Sonstiges</label>
</labels>
<input>
<ticket>Meine Karte wurde diesen Monat zweimal belastet.</ticket>
</input>
<output_format>
<classification>
<label_id one_of="bug|billing|feature|other">string</label_id>
<justification max_chars="180">Ein bis zwei Saetze.</justification>
</classification>
</output_format>
</prompt><prompt>
<persona>
<role>Customer Success Manager</role>
<tone>Professionell, hilfsbereit, knapp</tone>
<signature>Viele Gruesse, Alex (Customer Success)</signature>
</persona>
<task>Entwirf eine Antwort, die bestaetigt, beantwortet und naechste Schritte vorschlaegt.</task>
<input>
<email_from_customer><![CDATA[
Hallo, ich komme nach dem Zuruecksetzen meines Passworts nicht mehr ins Dashboard.
]]></email_from_customer>
</input>
<output_format>
<email>
<subject>string</subject>
<body>Klartext, kurze Absaetze</body>
<next_steps>
<step>Stichpunkt</step>
</next_steps>
</email>
</output_format>
</prompt><prompt>
<task>Erzeuge eine SQL-Abfrage aus einer natuerlichsprachlichen Frage und einem Schema.</task>
<schema><![CDATA[
tables:
orders(order_id, customer_id, total, created_at)
customers(customer_id, region)
]]></schema>
<input>
<question>Gesamtumsatz pro Region fuer das laufende Jahr 2024.</question>
</input>
<output_format>
<sql><![CDATA[
SELECT ...
]]></sql>
<assumptions max="3">
<item>kurzer Satz</item>
</assumptions>
<safety_checks>
<check>Sind die Datumsfilter korrekt?</check>
<check>Verbinden die Joins die richtigen Schluessel?</check>
</safety_checks>
</output_format>
</prompt><prompt>
<task>Wandle ein Meeting-Transkript in strukturierte Notizen um.</task>
<input>
<transcript><![CDATA[(Transkript einfuegen)]]></transcript>
</input>
<output_format>
<notes>
<topics>
<topic>...</topic>
</topics>
<decisions>
<decision>...</decision>
</decisions>
<action_items>
<item owner="Name" due="YYYY-MM-DD">...</item>
</action_items>
</notes>
</output_format>
</prompt><prompt>
<task>Leite die Nutzereingabe an den richtigen Handler weiter.</task>
<handlers>
<handler id="KB_SEARCH">Wissensdatenbank-Suche</handler>
<handler id="HUMAN_HANDOFF">Uebergabe an Mensch</handler>
<handler id="ORDER_STATUS">Bestellstatus</handler>
</handlers>
<input>
<utterance>Wo ist meine Bestellung Nr. 1234?</utterance>
</input>
<output_format>
<routing>
<handler_id one_of="KB_SEARCH|HUMAN_HANDOFF|ORDER_STATUS">ORDER_STATUS</handler_id>
<entities>
<order_id>1234</order_id>
</entities>
<confidence>0-1</confidence>
</routing>
</output_format>
</prompt><prompt>
<task>Kritisiere und verbessere den Entwurf, begrenze die Begruendung.</task>
<constraints>
<rationale_limit>120 Zeichen</rationale_limit>
</constraints>
<input>
<draft><![CDATA[(Entwurf einfuegen)]]></draft>
</input>
<output_format>
<improvement>
<revised><![CDATA[(ueberarbeiteter Text)]]></revised>
<rationale>max. 120 Zeichen</rationale>
<checks>
<check id="clarity">pass|fail</check>
<check id="tone">pass|fail</check>
<check id="length">pass|fail</check>
</checks>
</improvement>
</output_format>
</prompt>With the Python standard library you can read the XML output in just a few lines:
import xml.etree.ElementTree as ET
def parse_extraction(xml_text: str):
root = ET.fromstring(xml_text)
ext = root.find('extraction')
return {
"company": (ext.findtext('company') or '').strip(),
"product": (ext.findtext('product') or '').strip(),
"price": (ext.findtext('price') or '').strip(),
"sentiment": (ext.findtext('sentiment') or '').strip(),
"key_points": [e.text.strip() for e in ext.findall('key_points/bullet') if e.text]
}Validation checklist:
A few proven techniques make your prompts considerably more stable:
| Anti-pattern | Solution |
|---|---|
| "Think step by step and show your reasoning." | Ask for short key points or a checklist: <key_points max="5">. |
| Unnamed or generic tags like <section1>. | Use domain nouns: <risks>, <mitigations>, <decision>. |
| Instructions mixed with examples. | Put <instructions> and <examples> in separate blocks. |
| Free-text outputs when structure is needed. | Specify an <output_format> with required children. |
| Code embedded without CDATA. | Always wrap code, JSON and regex in CDATA. |
| Optional fields not specified. | Define how unknowns are represented (empty tag). |
Why XML instead of Markdown headings? XML is machine-parseable and consistent. Markdown is good for humans, but ambiguous for machines.
Can I mix XML with Markdown? Yes. Keep the machine-readable parts in XML, human notes can sit as bullet points inside <instructions>.
What about security and compliance? Use <checks> to enforce rules such as no personal data or no medical or legal advice. On a violation, output an <error>.
How do I avoid rambling justifications? Limit them via attributes such as max_chars or max and prefer checklists over prose.