Episode 4 — Generative AI Engineering / 4.3 — Prompt Engineering Fundamentals
4.3.d — Output Formatting Instructions
In one sentence: Specifying the exact output format in your prompt is what makes LLM responses machine-parseable — without format instructions, the model returns free-form prose that breaks your downstream code on every other call.
Navigation: ← 4.3.c — Chain-of-Thought · 4.3 Overview →
1. Why Predictable Response Format Matters
When humans read LLM output, any reasonable format works. When code reads LLM output, the format must be predictable. A single extra word, a missing comma, or an unexpected newline can crash your application.
// Your downstream code expects JSON
const result = JSON.parse(llmResponse);
// If the model returns "Here's the JSON: {"name": "Alice"}"
// → JSON.parse CRASHES because of "Here's the JSON: " prefix
// Your downstream code expects a number
const score = parseFloat(llmResponse);
// If the model returns "The score is 8.5 out of 10"
// → parseFloat returns NaN because of the prefix text
The entire purpose of output formatting instructions is to make LLM responses reliably parseable by code. This is not about aesthetics — it is about whether your application works.
┌─────────────────────────────────────────────────────────────────┐
│ WHY FORMAT MATTERS │
│ │
│ Human reads output: Code reads output: │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ "The sentiment │ │ JSON.parse( │ │
│ │ is positive, │ │ response │ │
│ │ with a score │ │ ) │ │
│ │ of 0.85" │ │ │ │
│ │ │ │ Expects: │ │
│ │ Human: "OK, it's │ │ {"sentiment": │ │
│ │ positive, 0.85" │ │ "positive", │ │
│ │ │ │ "score": 0.85} │ │
│ │ Works fine ✓ │ │ │ │
│ └──────────────────┘ │ Gets free text → 💥│ │
│ └──────────────────┘ │
│ │
│ RULE: If code will consume the output, you MUST specify format │
└─────────────────────────────────────────────────────────────────┘
2. Specifying JSON Format in Prompts
JSON is the most common output format for production AI systems because it is universally parseable, strongly structured, and integrates naturally with JavaScript and REST APIs.
Basic JSON specification
const systemPrompt = `You are a data extraction assistant.
Extract the requested information and return it as a JSON object.
Respond ONLY with valid JSON. No explanation, no markdown, no extra text.`;
const userMessage = `Extract the person's details from this text:
"Dr. Sarah Chen, age 42, is the CTO of TechCorp based in San Francisco."
Return JSON with keys: name, age, title, company, city`;
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
],
});
const data = JSON.parse(response.choices[0].message.content);
// { name: "Sarah Chen", age: 42, title: "CTO", company: "TechCorp", city: "San Francisco" }
Providing a schema with types
The more explicit your schema, the more consistent the output:
const userMessage = `Extract the event details from this text:
"Join us for ReactConf 2025 on June 15-17 at the Moscone Center.
Early bird tickets are $399, regular admission is $599."
Return JSON matching this EXACT schema:
{
"event_name": string,
"start_date": string (YYYY-MM-DD format),
"end_date": string (YYYY-MM-DD format),
"venue": string,
"tickets": [
{
"type": string,
"price": number (USD, no currency symbol)
}
]
}`;
Handling missing fields
Always specify what to do when information is missing:
const systemPrompt = `Extract contact information as JSON.
Schema: { "name": string, "email": string, "phone": string, "company": string }
Rules:
- If a field cannot be found in the text, use null (not "N/A", not "", not "unknown")
- Do not infer or guess missing information
- Respond ONLY with the JSON object`;
Arrays and nested objects
const systemPrompt = `Analyze the code and return issues as JSON.
Response schema:
{
"file": string,
"issues": [
{
"line": number,
"severity": "error" | "warning" | "info",
"message": string,
"suggestion": string
}
],
"summary": {
"total_issues": number,
"errors": number,
"warnings": number,
"info": number
}
}
Respond ONLY with valid JSON. No markdown code fences. No explanation.`;
3. Markdown, Bullet Points, and Tables
Not every output needs to be JSON. For user-facing responses, structured Markdown is often the best format — readable by humans and still parseable by code.
Bullet point format
const prompt = `List the pros and cons of using TypeScript in a startup.
Format your response EXACTLY as:
## Pros
- [pro 1]
- [pro 2]
- [pro 3]
## Cons
- [con 1]
- [con 2]
- [con 3]
Use exactly 3 pros and 3 cons. Each point should be one sentence.`;
Table format
const prompt = `Compare these JavaScript frameworks for building a dashboard.
Format your response as a Markdown table with these EXACT columns:
| Framework | Learning Curve | Performance | Ecosystem | Best For |
Include rows for: React, Vue, Svelte, Angular
Rate Learning Curve and Performance as: Low, Medium, High
Keep each cell under 10 words.`;
Section-based format
const systemPrompt = `You are a code review assistant.
Structure EVERY response with these exact Markdown headers:
## Summary
(1-2 sentence overview)
## Issues Found
(numbered list of issues, or "No issues found.")
## Suggestions
(numbered list of improvements, or "No suggestions.")
## Verdict
(one of: APPROVE, REQUEST_CHANGES, REJECT)
Do not add any other sections. Do not skip any section.`;
4. Delimiters for Structured Responses
Delimiters mark the boundaries of different sections in the output. They make it easy to parse specific parts programmatically.
XML-style delimiters
const systemPrompt = `Analyze the customer message and respond using these delimiters:
<sentiment>positive, negative, or neutral</sentiment>
<category>billing, technical, shipping, account, or other</category>
<priority>high, medium, or low</priority>
<suggested_response>Your suggested reply to the customer</suggested_response>
Include ALL four sections. Do not add any text outside the delimiters.`;
// Parsing the response
function parseResponse(response) {
const extract = (tag) => {
const match = response.match(new RegExp(`<${tag}>(.*?)</${tag}>`, 's'));
return match ? match[1].trim() : null;
};
return {
sentiment: extract('sentiment'),
category: extract('category'),
priority: extract('priority'),
suggestedResponse: extract('suggested_response'),
};
}
Section markers
const prompt = `Translate the text and provide context.
---TRANSLATION---
[The translated text goes here]
---NOTES---
[Any translation notes or cultural context]
---CONFIDENCE---
[high, medium, or low]
Text to translate: "${inputText}"`;
// Parsing
function parseSections(response) {
const sections = {};
const parts = response.split(/---(\w+)---/).filter(Boolean);
for (let i = 0; i < parts.length; i += 2) {
sections[parts[i].toLowerCase()] = parts[i + 1]?.trim();
}
return sections;
}
Triple backtick blocks
const prompt = `Fix the bug in this code.
Respond with:
1. A brief explanation of the bug (one sentence)
2. The corrected code in a code block
EXPLANATION: [your one-sentence explanation]
\`\`\`javascript
[your corrected code here]
\`\`\`
Do not include anything else.`;
// Parsing
function parseCodeResponse(response) {
const explanation = response.match(/EXPLANATION:\s*(.+)/)?.[1]?.trim();
const code = response.match(/```javascript\n([\s\S]*?)```/)?.[1]?.trim();
return { explanation, code };
}
5. The "Respond ONLY with..." Pattern
This is the single most important pattern for ensuring the model does not add extra text around your structured output. Without it, models love to add helpful prefixes like "Here's the JSON:" or postfixes like "Let me know if you need anything else!"
The pattern
"Respond ONLY with [format]. No explanation, no additional text, no markdown formatting."
Variations
// For JSON
const jsonInstruction = `Respond ONLY with a valid JSON object.
Do not wrap it in markdown code fences.
Do not add any text before or after the JSON.`;
// For a single value
const singleValueInstruction = `Respond with ONLY the category name.
One word. No punctuation. No explanation.`;
// For a list
const listInstruction = `Respond ONLY with the items, one per line.
No numbering. No bullet points. No headers. No explanation.`;
// For code
const codeInstruction = `Respond ONLY with the code.
No explanation. No markdown code fences.
No comments unless they were in the original code.`;
Why you need to be explicit about what NOT to include
Models are trained to be helpful, which means they default to adding context, explanations, and pleasantries. Each of these breaks JSON parsing:
WITHOUT "Respond ONLY with":
Prompt: "Convert this to JSON: name is Alice, age is 30"
Response: "Here's the JSON representation:
```json
{"name": "Alice", "age": 30}
Let me know if you need any changes!"
→ JSON.parse() fails on this entire string
WITH "Respond ONLY with valid JSON": Prompt: "Convert this to JSON: name is Alice, age is 30. Respond ONLY with valid JSON." Response: {"name": "Alice", "age": 30}
→ JSON.parse() works perfectly
### Reinforcing the pattern in the system prompt
```javascript
const messages = [
{
role: 'system',
content: `You are a JSON extraction API.
You ONLY output valid JSON.
Never include explanatory text.
Never wrap JSON in code fences.
Never say "here is" or "let me know".
Your entire response must be parseable by JSON.parse().`
},
{
role: 'user',
content: `Extract: "Meeting with Bob tomorrow at 3pm in Room 204"
Schema: { "person": string, "date": string, "time": string, "location": string }`
}
];
6. Combining Format Instructions with Few-Shot Examples
The most reliable way to ensure consistent output format is to combine explicit format instructions with few-shot examples that demonstrate the exact format. Instructions tell the model what to do; examples show what it looks like.
Pattern: Instructions + Examples + Format enforcement
const systemPrompt = `You are a receipt data extraction system.
Rules:
- Extract all items, their quantities, and prices
- Dates must be in YYYY-MM-DD format
- Prices must be numbers (no currency symbols)
- If a field is missing, use null
- Respond ONLY with valid JSON
Example 1:
Input: "STARBUCKS #1234
Date: 03/15/2024
Grande Latte x1 $5.75
Croissant x2 $3.50
Subtotal: $12.75
Tax: $1.02
Total: $13.77"
Output: {"store": "Starbucks", "date": "2024-03-15", "items": [{"name": "Grande Latte", "quantity": 1, "price": 5.75}, {"name": "Croissant", "quantity": 2, "price": 3.50}], "subtotal": 12.75, "tax": 1.02, "total": 13.77}
Example 2:
Input: "TARGET
01/22/2024
USB Cable $12.99
Phone Case $24.99
Total: $37.98"
Output: {"store": "Target", "date": "2024-01-22", "items": [{"name": "USB Cable", "quantity": 1, "price": 12.99}, {"name": "Phone Case", "quantity": 1, "price": 24.99}], "subtotal": null, "tax": null, "total": 37.98}`;
Notice how Example 2 demonstrates the null handling rule (missing subtotal and tax) and the implicit quantity of 1 when not specified. The instructions state the rules; the examples show them in action.
Building a robust extraction function
async function extractReceipt(receiptText) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `Extract from this receipt:\n\n${receiptText}` },
],
});
const content = response.choices[0].message.content.trim();
// Safety: strip markdown code fences if model adds them despite instructions
const cleaned = content
.replace(/^```json\n?/, '')
.replace(/\n?```$/, '')
.trim();
try {
return JSON.parse(cleaned);
} catch (error) {
console.error('JSON parse failed:', error.message);
console.error('Raw response:', content);
return null; // or throw, or retry
}
}
7. Common Formatting Failures and How to Prevent Them
Failure 1: Model adds explanatory text around JSON
Problem: "Here's the extracted data:\n{"name": "Alice"}\nHope this helps!"
Prevention:
// In system prompt
const prevention = `Your ENTIRE response must be valid JSON.
Do not include ANY text before or after the JSON object.
Do not say "here is", "sure", "hope this helps", or any other text.
ONLY output the JSON object.`;
Failure 2: Model wraps JSON in markdown code fences
Problem: ```json\n{"name": "Alice"}\n```
Prevention:
// In system prompt
const prevention = `Do not wrap JSON in markdown code fences.
Do not use triple backticks.
Output raw JSON directly.`;
// In parsing code (defense in depth)
function safeParseJSON(response) {
let cleaned = response.trim();
// Remove markdown code fences
cleaned = cleaned.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
return JSON.parse(cleaned.trim());
}
Failure 3: Model uses wrong field names
Expected: {"first_name": "Alice"}
Got: {"firstName": "Alice"} or {"name": "Alice"}
Prevention:
// Show the EXACT keys in the prompt
const prevention = `Return JSON with EXACTLY these keys (case-sensitive):
{
"first_name": string,
"last_name": string,
"email_address": string
}
Do not rename, combine, or add keys.`;
// Plus: validate in code
function validateKeys(obj, expectedKeys) {
const actualKeys = Object.keys(obj);
const missing = expectedKeys.filter(k => !actualKeys.includes(k));
const extra = actualKeys.filter(k => !expectedKeys.includes(k));
if (missing.length || extra.length) {
throw new Error(`Invalid keys. Missing: ${missing}. Extra: ${extra}`);
}
}
Failure 4: Model uses wrong data types
Expected: {"age": 30} (number)
Got: {"age": "30"} (string)
Got: {"age": "thirty"} (string, not even numeric)
Prevention:
// Be explicit about types
const prevention = `Schema (strict types):
{
"age": number (integer, not a string),
"price": number (decimal, e.g., 12.99, not "$12.99"),
"is_active": boolean (true or false, not "yes"/"no"),
"tags": string[] (array of strings, not comma-separated string)
}`;
Failure 5: Model returns array instead of object (or vice versa)
Expected: {"items": [{"name": "A"}, {"name": "B"}]}
Got: [{"name": "A"}, {"name": "B"}]
Prevention:
// Be explicit about the top-level structure
const prevention = `Return a JSON OBJECT (not an array) with this structure:
{
"items": [...],
"total_count": number
}
The top-level response MUST be an object with curly braces, not an array.`;
Failure 6: Inconsistent formatting across requests
The same prompt sometimes returns one format, sometimes another.
Prevention:
// 1. Temperature 0
// 2. Few-shot examples showing the exact format
// 3. "Respond ONLY with..." instruction
// 4. Validation + retry in code
async function extractWithRetry(input, maxRetries = 2) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await callLLM(input);
const parsed = safeParseJSON(response);
validateSchema(parsed); // throws if invalid
return parsed;
} catch (error) {
if (attempt === maxRetries) {
throw new Error(`Extraction failed after ${maxRetries + 1} attempts: ${error.message}`);
}
console.warn(`Attempt ${attempt + 1} failed, retrying...`);
}
}
}
Failure 7: Model returns empty or incomplete JSON
Expected: {"name": "Alice", "age": 30, "city": "NYC"}
Got: {"name": "Alice"} (missing fields)
Got: {} (empty)
Prevention:
// In prompt
const prevention = `ALL fields are REQUIRED. Include every key even if the value is null.
Never return a partial object. Never return an empty object.`;
// In code
function validateCompleteness(obj, requiredKeys) {
for (const key of requiredKeys) {
if (!(key in obj)) {
throw new Error(`Missing required key: ${key}`);
}
}
}
8. Format-Specific Best Practices
JSON best practices
// 1. Always use temperature 0 for JSON output
// 2. Always include "Respond ONLY with valid JSON"
// 3. Always show the schema with types
// 4. Always specify null handling
// 5. Always strip code fences in parsing
// 6. Always validate the parsed result
const JSON_SYSTEM_PROMPT = `You are a JSON API. Rules:
1. Your ENTIRE response is valid JSON
2. No text outside the JSON
3. No markdown code fences
4. All keys exactly as specified
5. null for missing values (not "N/A", not "", not undefined)
6. Numbers are numbers (not strings)
7. Booleans are true/false (not "yes"/"no")`;
Markdown best practices
// Use when output is for human reading (chat, docs, reports)
// Specify exact headers to ensure parseable structure
const MARKDOWN_SYSTEM_PROMPT = `Format all responses using Markdown.
Use ## for main sections, ### for subsections.
Use - for bullet lists, 1. for numbered lists.
Use **bold** for emphasis, \`code\` for inline code.
Use | for tables when comparing items.
Always use exactly the section headers specified in the user's request.`;
Single-value best practices
// When you only need one value back (a label, a number, a yes/no)
const SINGLE_VALUE_PROMPT = `Respond with ONLY the category name.
Nothing else. One word. No period. No explanation.
Categories: billing, technical, shipping, account, other`;
// Parse with trim and lowercase
function parseSingleValue(response, validValues) {
const value = response.trim().toLowerCase();
if (!validValues.includes(value)) {
throw new Error(`Unexpected value: "${value}". Expected one of: ${validValues.join(', ')}`);
}
return value;
}
9. Using API-Level Format Enforcement
Some LLM APIs provide built-in features to enforce output format. These are more reliable than prompt-level instructions alone.
OpenAI's response_format parameter
// Force JSON output at the API level
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
response_format: { type: 'json_object' }, // API-level enforcement
messages: [
{
role: 'system',
content: 'Extract the data as JSON. Return: { "name": string, "age": number }'
},
{
role: 'user',
content: 'Extract from: "Alice is 30 years old."'
},
],
});
// The response is GUARANTEED to be valid JSON
// (though field names and types still need validation)
const data = JSON.parse(response.choices[0].message.content);
OpenAI's structured outputs (JSON Schema)
// Even stricter: define the exact schema
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
response_format: {
type: 'json_schema',
json_schema: {
name: 'person_extraction',
strict: true,
schema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
email: { type: ['string', 'null'] },
},
required: ['name', 'age', 'email'],
additionalProperties: false,
},
},
},
messages: [
{ role: 'system', content: 'Extract person details from the text.' },
{ role: 'user', content: '"Alice, age 30, no email provided."' },
],
});
// Response GUARANTEED to match the schema
// { "name": "Alice", "age": 30, "email": null }
Defense in depth: API + Prompt + Code validation
async function extractPerson(text) {
// Layer 1: Prompt-level format instructions
const systemPrompt = `Extract person details. Return JSON:
{ "name": string, "age": number, "email": string | null }
Respond ONLY with JSON.`;
// Layer 2: API-level format enforcement
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text },
],
});
// Layer 3: Code-level validation
const data = JSON.parse(response.choices[0].message.content);
if (typeof data.name !== 'string') throw new Error('name must be string');
if (typeof data.age !== 'number') throw new Error('age must be number');
if (data.email !== null && typeof data.email !== 'string') {
throw new Error('email must be string or null');
}
return data;
}
10. Real-World Example: Building a Format-Reliable Pipeline
Here is a complete example of a production-quality extraction function that combines all the techniques from this lesson:
// config/prompts.js
export const INVOICE_EXTRACTION_SYSTEM = `You are an invoice data extraction system.
TASK: Extract structured data from invoice text.
OUTPUT FORMAT: Valid JSON matching this schema:
{
"vendor": string,
"invoice_number": string,
"date": string (YYYY-MM-DD),
"due_date": string (YYYY-MM-DD) or null,
"line_items": [
{
"description": string,
"quantity": number,
"unit_price": number,
"total": number
}
],
"subtotal": number,
"tax": number or null,
"total": number,
"currency": string (ISO 4217, e.g., "USD")
}
RULES:
- All prices as numbers (no currency symbols)
- Dates in YYYY-MM-DD format
- null for missing fields (not "N/A" or "")
- If quantity is not specified, assume 1
- Infer currency from context (default "USD" if unclear)
Respond ONLY with the JSON object. No explanation. No code fences.`;
// Example to include for complex invoices
export const INVOICE_EXAMPLE = `
Input: "INVOICE #2024-0892
Acme Corp
Date: March 15, 2024
Due: April 15, 2024
Web Development Services 40 hrs @ $150/hr $6,000.00
Domain Registration 1 yr $14.99
Subtotal: $6,014.99
Tax (8.5%): $511.27
Total: $6,526.26"
Output: {"vendor":"Acme Corp","invoice_number":"2024-0892","date":"2024-03-15","due_date":"2024-04-15","line_items":[{"description":"Web Development Services","quantity":40,"unit_price":150,"total":6000},{"description":"Domain Registration","quantity":1,"unit_price":14.99,"total":14.99}],"subtotal":6014.99,"tax":511.27,"total":6526.26,"currency":"USD"}`;
// services/invoiceExtractor.js
import { INVOICE_EXTRACTION_SYSTEM, INVOICE_EXAMPLE } from '../config/prompts.js';
async function extractInvoice(invoiceText) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: INVOICE_EXTRACTION_SYSTEM },
{ role: 'user', content: `Example:\n${INVOICE_EXAMPLE}\n\nNow extract:\nInput: "${invoiceText}"\nOutput:` },
],
});
const raw = response.choices[0].message.content;
// Parse with fallback
let data;
try {
data = JSON.parse(raw.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim());
} catch (e) {
throw new Error(`JSON parse failed: ${e.message}\nRaw: ${raw}`);
}
// Validate required fields
const required = ['vendor', 'invoice_number', 'date', 'line_items', 'total'];
for (const key of required) {
if (data[key] === undefined) {
throw new Error(`Missing required field: ${key}`);
}
}
// Validate types
if (!Array.isArray(data.line_items)) {
throw new Error('line_items must be an array');
}
if (typeof data.total !== 'number') {
throw new Error('total must be a number');
}
// Validate date format
if (data.date && !/^\d{4}-\d{2}-\d{2}$/.test(data.date)) {
throw new Error(`Invalid date format: ${data.date}. Expected YYYY-MM-DD.`);
}
return data;
}
11. Key Takeaways
- If code consumes the output, you MUST specify format — free-form prose breaks
JSON.parse(), regex parsers, and downstream pipelines. - "Respond ONLY with..." is the most important pattern — it prevents the model from adding helpful but destructive wrapper text.
- Show the schema with types — don't just say "return JSON"; specify every key name, type, and null handling rule.
- Few-shot examples + format instructions together are more reliable than either alone.
- Use API-level format enforcement (
response_format) when available — it is more reliable than prompt instructions. - Always validate in code — even with perfect prompts and API enforcement, validate the parsed output before using it.
- Defense in depth: prompt instructions + API enforcement + code validation = reliable formatting.
- Temperature 0 for any output that will be parsed by code — randomness and structure do not mix.
Explain-It Challenge
- A developer says: "I told the model to return JSON and it usually works. Why do I need all these extra steps?" Explain why "usually" is not good enough for production code.
- Your extraction pipeline fails at 2 AM because the model returned
{"price": "$12.99"}instead of{"price": 12.99}. Walk through every layer of defense that should have caught this. - Compare the reliability of these three approaches for getting JSON output: (a) prompt instruction only, (b)
response_format: json_object, (c)response_format: json_schemawith strict mode. What does each guarantee?
Navigation: ← 4.3.c — Chain-of-Thought · 4.3 Overview →