Single prompts hit a ceiling fast. The moment your task involves multiple reasoning steps, conditional logic, or iterating on output quality, you need prompt chaining — breaking complex work into a sequence of focused prompts where each step feeds the next.

This tutorial covers five battle-tested patterns that scale from personal scripts to production systems. Each pattern includes working code examples you can adapt immediately.

What Is Prompt Chaining?

Prompt chaining is the practice of decomposing a complex task into multiple sequential (or parallel) LLM calls, where the output of one prompt becomes the input to the next. Instead of cramming everything into a single mega-prompt, you design a pipeline of smaller, focused prompts that each do one thing well.

The benefits are concrete:

  • Reliability — each step has a narrow scope, reducing hallucination surface area
  • Debuggability — you can inspect intermediate outputs to find exactly where things go wrong
  • Flexibility — swap or modify individual steps without rewriting the entire pipeline
  • Cost control — use cheaper models for simple steps, expensive ones only where quality matters

Pattern 1: Sequential Refinement

The simplest chain pattern. Each step refines the output of the previous step through a generate-critique-revise loop.

Use case: Content generation, code writing, data transformation.

How it works:

  1. Generate a raw draft
  2. Critique the draft for specific issues
  3. Revise using the critique as a checklist
# Step 1: Generate raw draft
draft = llm_call(
    prompt=f"Write a technical explanation of {topic}. "
           f"Include code examples. Be thorough.",
    model="claude-sonnet"
)
 
# Step 2: Critique the draft
critique = llm_call(
    prompt=f"Review this draft for accuracy, clarity, and completeness. "
           f"List specific issues as a numbered list:\n\n{draft}",
    model="claude-sonnet"
)
 
# Step 3: Revise based on critique
final = llm_call(
    prompt=f"Revise this draft addressing every issue in the critique.\n\n"
           f"DRAFT:\n{draft}\n\nCRITIQUE:\n{critique}",
    model="claude-sonnet"
)

Why it works: The critique step forces a different evaluation mode than generation. Without it, asking the model to "improve" its own output tends to produce minimal changes. Explicit critique surfaces issues the generation pass glossed over.

Pattern 2: Gate-and-Route

A fast classification step decides which downstream chain executes. This keeps each branch simple, purpose-built, and easy to maintain independently.

Use case: Customer support routing, content moderation, multi-format document processing.

How it works:

  1. Classify the input using a cheap, fast model
  2. Route to the specialized handler for that class
# Step 1: Classify the input (use cheapest model here)
category = llm_call(
    prompt=f"Classify this request into exactly one category: "
           f"[billing, technical, account, other]\n\n"
           f"Respond with only the category name.\n\n{user_input}",
    model="claude-haiku",
    temperature=0  # deterministic classification
)
 
# Step 2: Route to specialized chain
if category.strip() == "billing":
    response = billing_chain(user_input)
elif category.strip() == "technical":
    response = technical_chain(user_input)
elif category.strip() == "account":
    response = account_chain(user_input)
else:
    response = general_chain(user_input)

Key insight: The classification gate only needs to output a single word. This is exactly the task where Haiku-class models excel — fast, cheap, and accurate for constrained output. Spending Opus tokens on classification is waste.

Pattern 3: Map-Reduce Synthesis

Split a large input into chunks, process each chunk independently, then synthesize the results. This breaks the context window ceiling and naturally enables parallelism.

Use case: Long document summarization, large codebase analysis, multi-source research compilation.

How it works:

  1. Split input into context-window-sized chunks
  2. Map: process each chunk independently (run in parallel)
  3. Reduce: synthesize all chunk results into a final output
import asyncio
 
# Step 1: Split input into manageable chunks
chunks = split_document(long_document, max_tokens=3000)
 
# Step 2: Map — process each chunk independently (run concurrently)
async def process_chunk(chunk):
    return await llm_call_async(
        prompt=f"Summarize the key points in this section. "
               f"Preserve specific data, numbers, and conclusions:\n\n{chunk}",
        model="claude-sonnet"
    )
 
summaries = await asyncio.gather(*[process_chunk(c) for c in chunks])
 
# Step 3: Reduce — synthesize all summaries into final output
final_summary = llm_call(
    prompt=f"Synthesize these section summaries into a cohesive overview. "
           f"Resolve any contradictions. Highlight the most critical findings:\n\n"
           + "\n\n---\n\n".join(summaries),
    model="claude-opus"  # best model for synthesis reasoning
)

Performance note: The map step is embarrassingly parallel. Running 10 chunks sequentially takes 10x as long as running them concurrently. Always use asyncio.gather() or a thread pool for the map phase. The synthesis step must run after all chunks complete.

Pattern 4: Verification Loop

Generate output, verify it against hard criteria, and iterate until it passes. This is how you get reliable structured output — particularly JSON — without resorting to brittle regex parsing or hoping the model gets it right on the first attempt.

Use case: JSON/schema output, code generation with tests, constraint satisfaction problems.

How it works:

  1. Generate output
  2. Validate with deterministic code (not another LLM call)
  3. If invalid, feed errors back and regenerate
  4. Repeat up to a fixed attempt limit
import json
import jsonschema
 
max_attempts = 3
schema = load_json_schema("output_schema.json")
last_error = None
 
for attempt in range(max_attempts):
    # Build prompt — include previous errors on retry
    prompt = f"Generate a JSON object matching this schema:\n{json.dumps(schema, indent=2)}\n\n"
    prompt += f"Requirements: {requirements}"
    if last_error:
        prompt += f"\n\nYour previous attempt failed validation with these errors:\n{last_error}\nFix them."
 
    output = llm_call(prompt=prompt, model="claude-sonnet", temperature=0)
 
    # Validate with deterministic code — not another LLM call
    try:
        parsed = json.loads(output)
        jsonschema.validate(parsed, schema)
        break  # success — exit loop
    except (json.JSONDecodeError, jsonschema.ValidationError) as e:
        last_error = str(e)
        if attempt == max_attempts - 1:
            raise RuntimeError(f"Failed after {max_attempts} attempts: {last_error}")

Critical rule: Always cap retries. If the model cannot produce valid output in 3 attempts, the prompt or schema needs redesign — not more retries. Infinite retry loops waste money and mask real problems.

Pattern 5: Parallel Fan-Out with Consensus

Run the same question through multiple models or configurations simultaneously, then synthesize the responses to surface agreement and disagreement. The result is more robust than any single model could produce.

Use case: High-stakes decisions, fact verification, architecture reviews, security assessments.

How it works:

  1. Fan out: same prompt to multiple models in parallel
  2. Include a deliberately skeptical configuration to stress-test assumptions
  3. Synthesize: identify consensus, resolve disagreements, produce final answer
import asyncio
 
async def get_perspectives(question: str) -> list[str]:
    tasks = [
        llm_call_async(prompt=question, model="claude-opus"),
        llm_call_async(prompt=question, model="chatgpt-4o"),
        llm_call_async(
            prompt=question,
            model="claude-sonnet",
            system="You are a skeptical reviewer. Actively challenge assumptions "
                   "and identify risks the other responses may overlook."
        ),
    ]
    return await asyncio.gather(*tasks)
 
responses = asyncio.run(get_perspectives(question))
 
# Synthesize with explicit consensus check
final = llm_call(
    prompt=f"Three independent experts answered the same question.\n\n"
           f"Expert A:\n{responses[0]}\n\n"
           f"Expert B:\n{responses[1]}\n\n"
           f"Expert C (skeptic):\n{responses[2]}\n\n"
           f"1. Identify points all three agree on.\n"
           f"2. Identify points of disagreement and explain why they differ.\n"
           f"3. Produce a final answer that incorporates the strongest arguments from each.",
    model="claude-opus"
)

Why include a skeptic: Without deliberate adversarial pressure, fan-out responses tend to agree — the same training data produces similar blind spots. The skeptic role forces the synthesis step to actively address counterarguments rather than averaging bland consensus.

Production Practices

These patterns compound well, but production systems require additional discipline:

1. Log every intermediate step. When a chain produces bad output, you need to know which step failed. Persist all intermediate results with timestamps and model versions — do not just log the final output.

2. Use structured formats between chain steps. JSON or XML at chain boundaries is easier to parse and validate than free-form text. Ambiguous hand-offs between steps are a major source of chain failures.

3. Set temperature to 0 for deterministic steps. Classification, extraction, and validation steps should be reproducible. Reserve temperature > 0 for creative generation steps only. Mixing them silently is a common bug.

4. Build incrementally. Implement step 1 first, verify it reliably, then add step 2. Do not design a 6-step chain on paper and implement it all at once — debugging a broken 6-step chain is far harder than debugging the step you just added.

5. Model tier allocation matters. Not every step needs your most expensive model. Classification and extraction run well on Haiku-class models. Synthesis and complex reasoning justify Opus-class spend. Allocating compute this way cuts costs by 40-70% in typical chains without quality loss.

Ready to build prompt chains?

Both Claude and ChatGPT provide the API access needed for prompt chaining. Claude excels at long-context synthesis steps; ChatGPT's broad tool ecosystem suits gate-and-route patterns well.

Get Started

When Not to Chain

Prompt chaining adds latency, complexity, and cost. Avoid it when:

  • A single well-crafted prompt already produces reliable results
  • Latency is the primary constraint and the steps cannot be parallelized
  • The task is genuinely atomic (simple classification, direct Q&A, single-format extraction)

The goal is reliability and control over a complex task — not adding pipeline complexity for its own sake. If a single prompt works, use it.