Index Ai Academy
Phase 1 · Building with LLMs · Prompting

Prompt chaining

Break big tasks into a pipeline of smaller model calls.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Real LLM features are rarely one prompt. A campaign brief becomes an outline. The outline becomes copy. The copy gets checked against brand rules, and only then does anything reach a customer. Chaining is how you get reliable output from work like that. Each model call does one job well. Your code can inspect every step. And when something goes wrong, you know exactly where.

What you'll be able to do

  • Split a big writing task into a pipeline of small model calls
  • Pass each step's output into the next step's prompt
  • Put checks in code between steps so bad output stops the chain
  • Decide when a chain beats a single prompt, and when it doesn't

What it covers

  1. One big prompt versus a chain
  2. A chain is a set of functions
  3. A chain with a check in the middle
  4. Gates: checks between steps
  5. Fan-out: the same step over many items
  6. Worked example: a two-step chain with a gate

One big prompt versus a chain

Think about cooking dinner for friends. You don't throw every ingredient into one pot at once. You chop, then cook, then taste, then adjust the seasoning. If the sauce comes out too salty, you know which step to fix.

Now say you want a launch email for a new rain jacket. You could send one prompt: "Read these product notes, decide the key benefits, write a subject line and a 120-word email in our brand voice, and make sure it doesn't mention competitors." Models can handle prompts like that. But the more jobs you pack into one call, the more likely one of them gets done badly. And you can't tell which one.

A prompt chain splits the work into steps, with one model call each:

  1. Extract: pull the three most important facts out of the product notes.
  2. Draft: write the email from those facts only.
  3. Check: test the draft against your rules.
  4. Revise: if the check fails, fix only what failed.

Each step's output becomes part of the next step's input. Your Python code is the glue. It calls the model, keeps each result in a variable, and decides what happens next.

Quick check
Each function below stands in for one step of a chain. What does this print?
def shout(text):
    return text.upper()

def add_excitement(text):
    return text + "!"

print(add_excitement(shout("big sale")))
  1. big sale!
  2. BIG SALE
  3. BIG SALE!
  4. big sale
Show the answer

BIG SALE!

The inner call runs first: shout turns "big sale" into "BIG SALE". That return value is the input to add_excitement, which adds the "!". A chain of model calls has the same shape.

A chain is a set of functions

You already know this pattern from Phase 0: break a job into functions that each do one thing. Here, each function happens to call a model.

First, a small helper called ask keeps the calls short. It takes a system prompt and a prompt, sends one request, and returns the text of the reply. Its third input, max_tokens, caps how long the reply can be. It's 500 tokens unless you say otherwise.

import requests

def ask(system, prompt, max_tokens=500):
    response = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"},
        json={"model": "claude-opus-5-5", "max_tokens": max_tokens, "system": system,
              "messages": [{"role": "user", "content": prompt}]},
        timeout=60,
    )
    response.raise_for_status()
    data = response.json()
    return "".join(b["text"] for b in data["content"] if b["type"] == "text")

Then each step is a function that builds its own prompt. It usually wraps the previous output in tags, such as <notes> and </notes>. The tags mark where the pasted material starts and ends. That makes the model much less likely to mistake it for instructions.

def extract_facts(notes):
    return ask("You pull facts out of product notes.",
               f"List the 3 facts a buyer would care most about.\n<notes>\n{notes}\n</notes>")

def draft_email(facts):
    return ask("You write launch emails for an outdoor clothing brand. Plain, warm, specific.",
               f"Write a 100-word launch email using only these facts.\n<facts>\n{facts}\n</facts>")

To run this chain, you'd write facts = extract_facts(notes) and then email = draft_email(facts). The text that the first step returns is the second step's input.

Worked example

A chain with a check in the middle

A chain's strength is that code can check each step before the next one starts. Here, step 1 extracts facts, code checks them, and step 2 only runs if they passed. The two model calls are stand-ins, so you can see the shape without an API key.

import json

def extract(ticket):                 # step 1: a model call in real code
    return '{"product": "tent", "problem": "torn zip"}'

def draft_reply(facts):              # step 2: another model call
    return f"Sorry about the {facts['problem']} on your {facts['product']}."

def run(ticket, extract=extract):
    raw = extract(ticket)
    try:
        facts = json.loads(raw)
    except ValueError:
        return "stopped at step 1: not JSON"
    if not facts.get("product"):
        return "stopped at step 1: no product"
    return draft_reply(facts)

print(run("My tent's zip tore."))
print(run("Help!", extract=lambda t: "I couldn't find anything."))
It runs right here in your browser, with nothing to install.

When step 1 goes wrong, the chain stops right there with a clear reason, and no money is spent on step 2. It's also easy to test each step alone. One long prompt doing both jobs couldn't tell you which part failed.

Try it now

A fake model stands in for the real one, so you can see the shape of a chain. Each step's return value becomes the next step's argument. The final check runs in plain Python. The last line prints: email(facts(rain shell)) True

Quick check
Why does draft_email put the facts between <facts> and </facts> tags?
  1. So the model can tell the pasted facts apart from your instructions
  2. Because the API rejects prompts without tags
  3. So Python can run the facts as code
  4. To make the reply shorter
Show the answer

So the model can tell the pasted facts apart from your instructions

The facts came from an earlier model call. They're material to work from, not orders to follow. The tags mark exactly where that material starts and ends. The API accepts prompts with or without tags.

Gates: checks between steps

Picture a factory line. An inspector stands between two machines. A part that fails inspection never reaches the next machine.

A chain gives you the same power. Your code can look at each step's output before moving on. These checks are called gates. Good gates are cheap tests written in plain Python:

  • Is the draft under the word limit?
  • Does it contain any banned words ("revolutionary", a competitor's name)?
  • Did the extraction step actually return three facts?
  • Is the output valid JSON, if you asked for JSON?

When a gate fails, you choose what happens next. You can retry the step. You can run a revision step that fixes only the problem. Or you can stop and flag the item for a person to review. One big prompt gives you none of those options.

For marketers: this is your content approval workflow, automated. Brief, draft, legal check, brand check, publish. Each stage has an owner and a pass or fail, and nothing skips ahead.

Quick check
This gate checks a draft for length and banned words. What does it print?
BANNED = ["epic", "revolutionary"]
draft = "An Epic jacket for wet days"
problems = []
if len(draft.split()) > 5:
    problems.append("too long")
for word in BANNED:
    if word in draft.lower():
        problems.append(f"uses {word}")
print(problems)
  1. ['too long']
  2. []
  3. ['too long', 'uses epic']
  4. ['uses epic']
Show the answer

['too long', 'uses epic']

The draft has six words, which is over the limit of 5. And draft.lower() turns "Epic" into "epic", so the banned-word test catches it, and problems ends as ['too long', 'uses epic']. Without .lower(), "Epic" would slip through.

Fan-out: the same step over many items

Chains often run one step over a whole list, then combine the results. Say you need descriptions for 40 products. You call draft_email once per product, in a loop. Then a final step checks the whole set for repetition, such as ten descriptions that all open the same way. This shape is sometimes called map-reduce. You map one step over many inputs. Then you reduce the results into one.

Go deeper Where the name map-reduce comes from

Both words come from programming. To map a function is to apply it to every item in a list. Python has a built-in map for this: map(len, ["a", "bb"]) gives the lengths 1 and 2. To reduce a list is to combine its items into one value, step by step. That's what sum does when it turns a list of numbers into a total. Large data systems use the same two steps to split work across many computers.

The costs of chaining

Every step is a separate request. So a four-step chain takes roughly four times as long as one call. It also resends some text each time, and you pay for that text again. Chains pay off when:

  • the task has distinct stages,
  • you need to check results partway through, or
  • one prompt keeps failing at one part of the job.

For a simple task, like a single subject line, one well-written prompt is better.

Quick check
One prompt takes about 3 seconds. You replace it with a four-step chain, and each call takes about as long as the old one. Roughly how long does the chain take?
  1. Under 1 second
  2. About 3 seconds
  3. About 7 seconds
  4. About 12 seconds
Show the answer

About 12 seconds

The steps run one after another, and each one is a separate request. Four calls of about 3 seconds each is about 12 seconds. That's the price of the extra control, so save chains for tasks that need it.

Worked example

Worked example: a two-step chain with a gate

Extract facts, draft an email from them, then check the draft in code. Without an API key, the replies are simulated. The chain still runs exactly as it would with real replies. Watch each step's output feed the next.

import requests

def ask(system, prompt, max_tokens=400):
    response = requests.post(
        "https://api.anthropic.com/v1/messages",
        headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"},
        json={"model": "claude-opus-5-5", "max_tokens": max_tokens, "system": system,
              "messages": [{"role": "user", "content": prompt}]},
        timeout=60,
    )
    response.raise_for_status()
    return "".join(b["text"] for b in response.json()["content"] if b["type"] == "text")

BANNED = ["revolutionary", "game-changing", "epic"]

def gate(email):
    problems = []
    if len(email.split()) > 120:
        problems.append("too long")
    for word in BANNED:
        if word in email.lower():
            problems.append(f"uses '{word}'")
    return problems

notes = "Packable rain shell, 280 g, fully taped seams, packs into its own pocket, $129."
facts = ask("You pull facts out of product notes.", f"List the 3 facts a buyer cares about most.\n<notes>{notes}</notes>")
print("STEP 1 facts:\n", facts[:140], "\n")
email = ask("You write launch emails. Plain, warm, specific.", f"Write a 100-word email from these facts only.\n<facts>{facts}</facts>")
print("STEP 2 email:\n", email[:140], "\n")
problems = gate(email)
print("STEP 3 gate:", "passed" if not problems else problems)
In the lesson you can change this and run it.

The first call's output, facts, is pasted into the second call's prompt inside <facts> tags. The [:140] slices keep the printout short: they show only the first 140 characters of each reply.

The gate is ordinary Python with no model call. So it's free and instant, and it never gets "creative." With a real key, step 1 returns three facts. Step 2 returns an email built only from them. If the gate had found a problem, the next step would be a revision call that quotes the problems back to the model.

Quick check
Why put the brand-rule check in Python code rather than asking the model "did you follow the rules?"
  1. Python checks are more creative
  2. The API doesn't allow two calls in a row
  3. Models can't read their own output
  4. A Python check gives the same answer every time, costs nothing and can't be talked out of a rule
Show the answer

A Python check gives the same answer every time, costs nothing and can't be talked out of a rule

Some checks need judgment, and those belong to a model. (Phase 1 covers LLM-as-judge.) But anything you can test exactly, like word counts and banned words, should be plain code.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 4 review cards for spaced repetition

Create a free account

Try a quiz question

Which task benefits most from a prompt chain?

  1. Turning 40 product spec sheets into checked, on-brand descriptions
  2. Writing one subject line for tomorrow's newsletter
  3. Translating a single word from English into Spanish
  4. Asking the model what CTR stands for in advertising

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson