Yesterday's chains were two steps. Today: a plan of N steps that you execute in a loop. The plan IS the structured output of step 1.
import json
from pydantic_ai import Agent
from pydantic import BaseModel
class Plan(BaseModel):
steps: list[str]
# Step 1: produce a plan
goal = "Compute the sum of the first 3 even numbers (2, 4, 6)."
plan = Agent(model, output_type=Plan).run_sync(
f"Break this goal into 2-4 short steps. Goal: {goal}"
).output
print("PLAN:")
for i, step in enumerate(plan.steps, 1):
print(f" {i}. {step}")
# Step 2: execute each step
results = []
for step in plan.steps[:3]: # cap loop count for cost
out = Agent(model).run_sync(f"Execute this step in one short sentence: {step}").output.strip()
results.append(out)
# Step 3: summarise
summary = Agent(model).run_sync(
f"Goal: {goal}\n\nStep results:\n" + "\n".join(f"- {r}" for r in results) +
"\n\nWrite a one-sentence summary that includes the final number."
).output
print("\nSUMMARY:", summary)Three jobs: plan, execute (looped), summarise. Each is a separate LLM call. The structure is enforced by the typed Plan output.
Right. The plan is data — plan.steps is a list. You loop over it. Each step is one LLM call. Then a final summary call ties everything together. Same chain pattern as L9, generalised to a variable number of middle steps.
What's the cost?
1 (plan) + N (execute, capped at 3) + 1 (summarise) = ~5 calls for our toy goal. Real planning chains can balloon — that's why production agents add cost caps and step caps. We cap at 3 here so the lesson is bounded.
goal
->
LLM #1 (planner) -> Plan(steps=[s1, s2, s3])
->
for each step:
LLM #N (executor) -> step result
->
LLM #last (summariser) -> final answer
Three roles. The middle role runs in a loop over the plan.
total_calls = 1 + len(plan.steps) + 1
Cap len(plan.steps) at execution time:
for step in plan.steps[:MAX_STEPS]:
...Without the cap, the planner can produce 20 steps and you've burned 22 quota slots on a question that didn't need it. Cap at 3-5 for non-trivial tasks; tune up if quality is bad.
class Plan(BaseModel):
steps: list[str]Guarantees you get a list of strings. Without it: free-text response that you'd re.split or .splitlines() and hope for the best. Typed output makes the loop trivial.
They aren't agents. The planner doesn't react to step results — the steps are decided up front. If a step's result should change the plan, you want an agent loop (week 3), not a planning chain.
Useful heuristic: if the steps are knowable from the goal alone, use a planning chain. If the steps depend on what earlier steps produce, use an agent.
A toy goal — sum of 2, 4, 6. The model produces a 2-3 step plan, you execute (capped at 3), then summarise. Verification asserts the summary mentions "12" (the answer).
Yesterday's chains were two steps. Today: a plan of N steps that you execute in a loop. The plan IS the structured output of step 1.
import json
from pydantic_ai import Agent
from pydantic import BaseModel
class Plan(BaseModel):
steps: list[str]
# Step 1: produce a plan
goal = "Compute the sum of the first 3 even numbers (2, 4, 6)."
plan = Agent(model, output_type=Plan).run_sync(
f"Break this goal into 2-4 short steps. Goal: {goal}"
).output
print("PLAN:")
for i, step in enumerate(plan.steps, 1):
print(f" {i}. {step}")
# Step 2: execute each step
results = []
for step in plan.steps[:3]: # cap loop count for cost
out = Agent(model).run_sync(f"Execute this step in one short sentence: {step}").output.strip()
results.append(out)
# Step 3: summarise
summary = Agent(model).run_sync(
f"Goal: {goal}\n\nStep results:\n" + "\n".join(f"- {r}" for r in results) +
"\n\nWrite a one-sentence summary that includes the final number."
).output
print("\nSUMMARY:", summary)Three jobs: plan, execute (looped), summarise. Each is a separate LLM call. The structure is enforced by the typed Plan output.
Right. The plan is data — plan.steps is a list. You loop over it. Each step is one LLM call. Then a final summary call ties everything together. Same chain pattern as L9, generalised to a variable number of middle steps.
What's the cost?
1 (plan) + N (execute, capped at 3) + 1 (summarise) = ~5 calls for our toy goal. Real planning chains can balloon — that's why production agents add cost caps and step caps. We cap at 3 here so the lesson is bounded.
goal
->
LLM #1 (planner) -> Plan(steps=[s1, s2, s3])
->
for each step:
LLM #N (executor) -> step result
->
LLM #last (summariser) -> final answer
Three roles. The middle role runs in a loop over the plan.
total_calls = 1 + len(plan.steps) + 1
Cap len(plan.steps) at execution time:
for step in plan.steps[:MAX_STEPS]:
...Without the cap, the planner can produce 20 steps and you've burned 22 quota slots on a question that didn't need it. Cap at 3-5 for non-trivial tasks; tune up if quality is bad.
class Plan(BaseModel):
steps: list[str]Guarantees you get a list of strings. Without it: free-text response that you'd re.split or .splitlines() and hope for the best. Typed output makes the loop trivial.
They aren't agents. The planner doesn't react to step results — the steps are decided up front. If a step's result should change the plan, you want an agent loop (week 3), not a planning chain.
Useful heuristic: if the steps are knowable from the goal alone, use a planning chain. If the steps depend on what earlier steps produce, use an agent.
A toy goal — sum of 2, 4, 6. The model produces a 2-3 step plan, you execute (capped at 3), then summarise. Verification asserts the summary mentions "12" (the answer).
Create a free account to get started. Paid plans unlock all tracks.