teachyou.ai academy
← All posts
Prompt EngineeringLLM workflowsagent designprompt design

Prompt Chaining Patterns for Complex Tasks

Pramod Dutta · Jun 22, 2026 · 10 min read

Prompt chaining is the practice of splitting one big task into a sequence of smaller LLM calls, where the output of one call becomes the input to the next. Instead of asking a model to draft a blog post, fact-check it, and format it in a single mega-prompt, you run three separate calls, each with a narrow job. This guide covers the patterns that actually hold up when the task gets complex: linear chains, branching chains, validation loops, and map-reduce style fan-out. Every pattern below includes runnable code you can adapt.

Why prompt chaining beats one giant prompt

A single prompt asking a model to do five things at once tends to do all five things at a mediocre level. The model has to hold every instruction in its attention at once, and errors in step two silently corrupt steps three through five with no place to catch them. Prompt chaining fixes this by giving each step its own context window, its own instructions, and its own chance to be checked before the next step runs.

The tradeoff is latency and cost: more calls mean more round trips. For interactive use cases, that matters. For batch or async workflows, it usually doesn't. The rule of thumb: chain when a task has natural checkpoints where you'd want to inspect, retry, or route the output differently. Don't chain when the task is genuinely a single cohesive judgment call, since splitting it just adds overhead without adding accuracy.

Pattern 1: Linear chaining (pipeline)

The simplest chain is a straight line: step A's output feeds step B, which feeds step C. Each step does one thing well.

A common example is content generation: outline, then draft, then edit.

import anthropic

client = anthropic.Anthropic()

def call(prompt, system=None):
    msg = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=2000,
        system=system or "",
        messages=[{"role": "user", "content": prompt}],
    )
    return msg.content[0].text

def generate_article(topic):
    outline = call(
        f"Create a 5-point outline for an article about: {topic}",
        system="You are an editorial planner. Output only the outline, numbered.",
    )

    draft = call(
        f"Write a full article following this outline:\n{outline}",
        system="You are a technical writer. Write in plain, direct prose.",
    )

    edited = call(
        f"Edit this draft for clarity and remove filler:\n{draft}",
        system="You are a copy editor. Return only the edited text.",
    )

    return edited

result = generate_article("why database indexes speed up reads")
print(result)

Each step is a separate messages.create call with its own system prompt. This is the whole idea of prompt chaining: narrow instructions per step, clean handoff of output to input.

Pattern 2: Chaining with validation gates

A linear chain becomes more reliable when you insert a check between steps instead of blindly passing output forward. The gate can be another LLM call (an "LLM as judge" step) or a plain code check.

def generate_with_gate(topic):
    draft = call(f"Write a 200-word summary of: {topic}")

    # validation gate: does the draft actually contain the topic?
    check = call(
        f"Does this text correctly and specifically address the topic '{topic}'? "
        f"Answer with only YES or NO.\n\nText: {draft}"
    )

    if check.strip().upper() != "YES":
        # retry once with a stricter instruction
        draft = call(
            f"Write a 200-word summary of: {topic}. "
            f"Be specific and stay strictly on topic."
        )

    return draft

This pattern catches drift before it propagates. It's cheap insurance: one extra call that can save a full re-run downstream. In production pipelines, put a validation gate after any step whose failure is expensive to discover later, such as a step that writes to a database or triggers an external action.

Pattern 3: Branching chains (conditional routing)

Not every task follows the same path. A support-ticket triager, for instance, needs to route differently depending on ticket type. This is a branching chain: one classification call decides which sub-chain runs next.

def handle_ticket(ticket_text):
    category = call(
        f"Classify this support ticket into exactly one word: "
        f"BILLING, BUG, or QUESTION.\n\nTicket: {ticket_text}"
    ).strip().upper()

    if category == "BILLING":
        return call(
            f"Draft a billing support reply for this ticket, referencing "
            f"our refund policy: {ticket_text}",
            system="You are a billing support agent.",
        )
    elif category == "BUG":
        return call(
            f"Extract reproduction steps and severity from this bug report: {ticket_text}",
            system="You are a triage engineer preparing a bug ticket.",
        )
    else:
        return call(
            f"Answer this product question concisely: {ticket_text}",
            system="You are a helpful product support agent.",
        )

The classification step is intentionally constrained to output one of a fixed set of labels, which makes the routing logic in your code deterministic. Never let a routing step return free text you then have to parse with regex; force it into an enum-like output.

Pattern 4: Map-reduce chaining for large inputs

When the input is too large for one context window, or when you want independent analysis of many pieces before combining, use a map-reduce chain: map the same prompt over each chunk, then reduce the results with a final call.

def summarize_long_document(chunks):
    # map: summarize each chunk independently
    chunk_summaries = [
        call(f"Summarize this section in 3 sentences:\n{chunk}")
        for chunk in chunks
    ]

    # reduce: combine the summaries into one
    combined = "\n\n".join(chunk_summaries)
    final_summary = call(
        f"Combine these section summaries into one coherent overview:\n{combined}"
    )
    return final_summary

For real workloads, run the map step concurrently rather than in a loop, since each chunk summary is independent:

import concurrent.futures

def map_reduce_summarize(chunks):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        chunk_summaries = list(
            executor.map(
                lambda c: call(f"Summarize this section in 3 sentences:\n{c}"),
                chunks,
            )
        )

    combined = "\n\n".join(chunk_summaries)
    return call(f"Combine these section summaries into one coherent overview:\n{combined}")

This pattern is the backbone of most document-QA and long-form-analysis systems: split, analyze in parallel, synthesize.

Pattern 5: Iterative refinement loops

Some tasks need repeated passes until a quality bar is met, not a fixed sequence. Code generation with test feedback is the canonical case: generate, run tests, feed failures back, regenerate.

def generate_function_with_tests(spec, test_code, max_attempts=3):
    code = call(f"Write a Python function matching this spec:\n{spec}")

    for attempt in range(max_attempts):
        # run the generated code against the tests (simplified)
        namespace = {}
        try:
            exec(code, namespace)
            exec(test_code, namespace)
            return code  # tests passed
        except Exception as e:
            code = call(
                f"This code failed with error: {e}\n\n"
                f"Code:\n{code}\n\nSpec:\n{spec}\n\n"
                f"Fix the code to satisfy the spec and pass the tests."
            )

    return code  # return best effort after max_attempts

Always cap the loop with max_attempts. An unbounded refinement loop is a silent cost leak, and models sometimes oscillate between two wrong answers instead of converging. A hard cap plus a fallback to "best effort" or a human handoff keeps the chain predictable.

Note: exec on model-generated code is shown here for illustration only. In production, run generated code in an isolated sandbox, never in the same process as your application.

Pattern 6: Chaining with structured handoffs

Passing raw text between chain steps works for simple cases, but as chains grow, structured output (JSON) between steps makes the handoff far less fragile. Ask each step to emit a fixed schema, then pass the parsed object forward instead of a paragraph the next step has to re-interpret.

import json

def extract_and_act(email_text):
    extraction = call(
        f"Extract the following from this email as JSON with keys "
        f"'sender_intent', 'urgency' (low/medium/high), and 'action_needed': "
        f"\n\n{email_text}\n\nReturn only valid JSON, no other text."
    )

    data = json.loads(extraction)

    if data["urgency"] == "high":
        return call(f"Draft an urgent same-day reply for: {data['action_needed']}")
    else:
        return call(f"Draft a standard reply for: {data['action_needed']}")

When a step must return JSON, say so explicitly and validate it with json.loads before trusting it downstream. Wrap the parse in a try/except and retry once with an even stricter "return only valid JSON" instruction if it fails; model output occasionally wraps JSON in prose or code fences even when told not to.

Building chains with an orchestration framework vs. raw code

The examples above use plain Python functions calling the API directly, which is enough for most chains up to five or six steps. Once a chain grows branches, retries, and shared state across many steps, an orchestration layer (LangGraph, a workflow engine, or even a simple state machine you write yourself) earns its keep by giving you:

  • Persistent state between steps, so a chain can pause and resume
  • Built-in retry and timeout handling per step
  • Visibility into which step failed and why, which raw function chains don't give you for free

Start with plain functions. Reach for a framework only when the chain's control flow (not the prompt content) becomes the hard part.

Common mistakes in prompt chaining

Passing too much context forward. Each step in a chain doesn't need the full history of every prior step, only what it needs to do its job. Trim aggressively; a bloated context window slows every downstream call and dilutes the model's attention on the current instruction.

No error boundary between steps. If step 2 fails (API error, malformed output, timeout), do not let step 3 run on garbage. Wrap each step and decide explicitly: retry, fall back, or abort the chain.

Treating the chain as fire-and-forget. Log the input and output of every step during development. When a chain misbehaves, the fastest way to find the broken link is to look at exactly what each step produced, not to re-run the whole chain and guess.

Chaining when one prompt would do. If two steps never need independent inspection, retries, or routing, merging them into one prompt is often faster and cheaper with no accuracy loss. Chain because the task has real seams, not by default.

FAQ

What is prompt chaining? Prompt chaining is splitting a task into multiple sequential LLM calls, where each call's output feeds the next call's input, instead of solving the whole task in one prompt.

When should I use prompt chaining instead of one long prompt? Use chaining when the task has natural checkpoints where you'd want to validate, retry, or branch the output differently. If the task is a single cohesive judgment with no useful checkpoint, one prompt is usually simpler and cheaper.

Does prompt chaining cost more than a single prompt? Yes, in raw API calls, since you're making multiple requests instead of one. In practice it often costs less overall because narrower prompts need fewer retries and produce fewer downstream errors that would otherwise require a full re-run.

How do I handle errors in the middle of a chain? Wrap each step so a failure is caught explicitly, then decide per step whether to retry with a stricter prompt, fall back to a default, or abort the whole chain rather than letting bad output silently flow to the next step.

Should chain steps pass plain text or structured JSON? Structured JSON is more reliable for chains longer than two or three steps, since it removes ambiguity about what the next step should extract from the previous step's output. Always validate the JSON before trusting it downstream.

Can chain steps run in parallel? Yes, when steps are independent, such as summarizing multiple document chunks before a final reduce step. Use a thread pool or async calls for the independent steps, and only serialize the steps that genuinely depend on each other's output.

What's the difference between prompt chaining and an agent? A chain follows a fixed or lightly-branching sequence of steps you define in code. An agent decides its own next action at each step, often choosing which tool to call based on the model's own reasoning. Chains are more predictable and easier to debug; agents are more flexible for open-ended tasks.

Prompt Chaining Patterns for Complex Tasks · TeachYou Academy