Skip to main content
Lesson 15 of 25 · Building OpenAI Agents

Multi-step agent with reasoning loop

Lesson 15 of 25 Updated July 29, 2026 Developer
On this page
  1. Multi-step agent with reasoning loop
  2. The plan-execute pattern
  3. Self-correcting agent
  4. Pattern: chain of specialized agents
  5. Controlling reasoning depth
  6. Key takeaways

Multi-step agent with reasoning loop

The most powerful agents don’t stop after a single tool call. They chain multiple reasoning steps, adjust their strategy based on results, and iterate until they reach their goal. In this lesson, you’ll learn how to build these multi-step agents. The examples use the GPT-5.6 family: where the previous generation required switching models to enable reasoning, you now control this behavior via the effort setting on your chosen model.

The plan-execute pattern

A multi-step agent starts by creating a plan, then executes each step. This separation is deliberate: the planner has no tools and produces a typed structure, while the executor receives this plan as a roadmap and simply follows it. A vague request like “prepare the quarterly review for key accounts” becomes a concrete sequence of actions—query the CRM, generate the report, send the email—before any action is attempted.

from agents import Agent, Runner, function_tool
from pydantic import BaseModel
import json

class PlanAction(BaseModel):
    steps: list[str]
    objective: str

planner_agent = Agent(
    name="Planner",
    instructions="""You receive a complex task. Break it down into simple, ordered steps.
    Each step must be a concrete, actionable item.""",
    model="gpt-5.6-sol",  # Advanced reasoning for planning
    output_type=PlanAction,
)

@function_tool
def search_crm(query: str) -> str:
    """Searches the CRM for customer information."""
    return json.dumps({"customers": [
        {"name": "Acme Corp", "revenue": 500000, "contract": "Enterprise"},
        {"name": "TechStart", "revenue": 50000, "contract": "Starter"},
    ]})

@function_tool
def generate_report(title: str, content: str) -> str:
    """Generates a formatted report."""
    return f"Report '{title}' generated successfully ({len(content)} characters)"

@function_tool
def send_email(recipient: str, subject: str, body: str) -> str:
    """Sends an email."""
    return f"Email sent to {recipient}: {subject}"

executor_agent = Agent(
    name="Executor",
    instructions="""You execute tasks step by step.
    Use available tools to complete each step.
    If a step fails, try an alternative approach.""",
    tools=[search_crm, generate_report, send_email],
    model="gpt-5.6-terra",
)

async def process_complex_task(task: str):
    # Step 1: Plan
    plan = await Runner.run(planner_agent, task)
    print(f"Plan: {plan.final_output.steps}")

    # Step 2: Execute with the plan as context
    execution_instructions = f"""Execute the following steps in order:
    {chr(10).join(f'{i+1}. {e}' for i, e in enumerate(plan.final_output.steps))}
    Objective: {plan.final_output.objective}"""

    result = await Runner.run(executor_agent, execution_instructions)
    return result.final_output

Self-correcting agent

Planning isn’t enough: in the real world, tools fail. The second form of looping lets the agent detect its own errors and correct them. Everything hinges on a detail often overlooked—the tool doesn’t raise an exception; it returns a readable error message. The agent reads it, realizes it forgot the FROM clause, rewrites its query, and retries, within the limit of three attempts set by its instructions.

from agents import Agent, Runner, function_tool

@function_tool
def execute_sql_query(sql: str) -> str:
    """Executes an SQL query on the database."""
    # Simulation: some queries fail
    if "DROP" in sql.upper():
        return "ERROR: Destructive queries are forbidden."
    if "SELECT" in sql.upper() and "FROM" not in sql.upper():
        return "ERROR: Invalid SQL syntax - missing FROM."
    return json.dumps({
        "columns": ["name", "monthly_revenue", "region"],
        "rows": [
            ["Acme Corp", 45000, "Île-de-France"],
            ["TechStart", 12000, "Rhône-Alpes"],
        ],
        "result_count": 2
    })

analyst_agent = Agent(
    name="Data Analyst",
    instructions="""You analyze data by executing SQL queries.
    If a query fails:
    1. Analyze the error message
    2. Correct the query
    3. Retry
    Maximum 3 attempts per query.
    Never use DROP, DELETE, or TRUNCATE.""",
    tools=[execute_sql_query],
    model="gpt-5.6-terra",
    max_turns=10,  # Enough turns for corrections
)

result = Runner.run_sync(
    analyst_agent,
    "What is the total revenue by region?"
)

Notice that the prohibition on destructive queries appears in two places: in the instructions and in the tool code. The instruction guides the model, but only the server-side check in the tool guarantees that a DROP TABLE never slips through.

Pattern: chain of specialized agents

For complex workflows, chain several specialized agents instead of loading a single agent with every responsibility. Each link has a clear role: the collector gathers the raw material, the analyst—equipped with a reasoning model—identifies trends, and the writer formats the output. The benefit is both educational and operational: when the final report disappoints, you know immediately which of the three agents to fix.

from agents import Agent, Runner

collection_agent = Agent(
    name="Data Collector",
    instructions="Collect all data necessary for the analysis.",
    tools=[search_crm, execute_sql_query],
    model="gpt-5.6-terra",
)

analysis_agent = Agent(
    name="Analyst",
    instructions="Analyze the collected data and identify trends.",
    model="gpt-5.6-sol",  # Advanced reasoning for analysis
)

writing_agent = Agent(
    name="Writer",
    instructions="""Write a clear, structured report in English
    based on the provided analysis. Format: introduction, key points, recommendations.""",
    model="gpt-5.6-terra",
)

async def analysis_pipeline(question: str):
    # Step 1: Collect data
    data = await Runner.run(collection_agent, question)

    # Step 2: Analyze
    analysis = await Runner.run(
        analysis_agent,
        f"Analyze this data: {data.final_output}"
    )

    # Step 3: Write the report
    report = await Runner.run(
        writing_agent,
        f"Write a report based on this analysis: {analysis.final_output}"
    )

    return report.final_output

Controlling reasoning depth

All these loops share one trait: they might never stop. The max_turns parameter is your primary safeguard, and you tune it based on the task’s ambition. Three turns suffice for an agent expected to answer on the first try; twenty-five are needed for an exploration that queries multiple sources before concluding.

# Conservative agent: few turns
simple_agent = Agent(
    name="Simple Agent",
    instructions="Answer in a single turn if possible.",
    model="gpt-5.6-terra",
)
result = await Runner.run(simple_agent, "Simple question", max_turns=3)

# Complex agent: many turns
research_agent = Agent(
    name="Deep Researcher",
    instructions="Explore all angles before concluding.",
    tools=[search_crm, execute_sql_query],
    model="gpt-5.6-terra",
)
result = await Runner.run(research_agent, "Complete Q1 analysis", max_turns=25)

Set this value by observing your traces rather than guessing: if your actual executions finish in six turns, a limit of twenty-five protects against nothing.

Key takeaways

  • The plan-execute pattern separates planning (reasoning model) from execution
  • A self-correcting agent analyzes its errors and adjusts its strategy
  • Chains of specialized agents enable complex, maintainable workflows
  • max_turns controls reasoning depth and protects against infinite loops
  • Reserve advanced reasoning capabilities for planning and analysis