CodeGym /Courses /ChatGPT Apps /Multi-step processes: model auto-orchestration and loop c...

Multi-step processes: model auto-orchestration and loop control

ChatGPT Apps
Level 12 , Lesson 3
Available

1. What a multi-step run is and how it differs from a “one-off” request

When you only worked with the ChatGPT App and MCP tools, the picture was fairly linear: a user request arrived → GPT decided to call one or more tools → you returned an answer to the user. This can still be considered “one logical step,” even if you did something more complex inside the tool.

For an agent, a run is a goal + a series of steps. We no longer think in “one prompt — one answer” terms; instead we treat the task as a mini-project that the agent drives from start to finish.

You can think of the difference like this:

Interaction type What the model does Where the logic lives
Typical tool call in the ChatGPT App Decides whether to call a tool, fills in arguments, and forms the answer based on the result Core business logic and sequence of actions live in a single tool or in the backend
Agent run (Agents SDK) Plans multiple steps, decides when and which tool to call, analyzes intermediate results, and can revise the plan The logic of “how to move toward the goal” is partly in the agent’s system instruction and partly emerges in the model’s head

Here is the key point: you don’t have to hand over planning entirely to the model. It’s typically a hybrid: you hard-code the major phases of the scenario (for example, “first gather requirements, then select gifts, then prepare the card”), and within each phase you allow the agent to freely use its tools.

A mini-analogy

A one-off tool call is like calling a courier: “pick up one document and bring it to the office.”

A multi-step agent run is like a personal assistant: “Prepare a gift for a colleague’s birthday: find out what they like, pick a few options, check delivery, and put it all together into a nice presentation.” The assistant decides which actions to take along the way.

A bit later in the lecture, we’ll also see how such multi-step runs fit into the Apps SDK → MCP → backend stack you already know, so that for ChatGPT and a widget the agent logic looks like a clean, ordinary MCP tool.

2. How the model plans steps on its own: a high-level view

In Agents SDK terms, each run can be conveniently represented as a triple:

  1. Goal: a textual description of the task that goes into the agent’s system/user instructions.
  2. Tools: a set of available tools with good descriptions and JSON Schema.
  3. State: the history of steps and structured state that you store externally (DB, Redis, whatever).

Then the familiar run loop starts: the model looks at the goal and available tools and on each step decides:

  • “I have enough information now — I can return the final result to the user”;
  • or “I need to call the X tool with these arguments”;
  • or “I got the tool result; now I need to interpret it, filter it, and possibly call another tool.”

At the pseudocode level, the idea looks like this (remember, this is a mental model, not a real API):

while (!done && steps < MAX_STEPS) {
  const modelResponse = await callModel({
    system: agentPolicy,
    messages: history,
    tools,
  });

  if (modelResponse.type === "tool_call") {
    const toolResult = await callTool(modelResponse.toolName, modelResponse.args);
    history.push({ role: "tool", content: toolResult });
  } else {
    // final answer
    done = true;
    return modelResponse.content;
  }

  steps++;
}

In a real Agents SDK, this entire loop is already implemented and “hidden” inside the library. You define the agent declaratively, and the SDK cycles the model and tools until it gets a final answer or hits step/time limits.

The architect’s job is to:

  • formulate the goal and system instruction so the model plans reasonable steps;
  • build a toolset without semantic overlap;
  • set limits on steps and time;
  • think through which steps can be parallelized.

Once we have a goal, tools, and a notion of state, the next question is what steps to take to reach that goal. Not all steps are the same: some are strictly sequential, while others can be parallelized.

3. Sequential and parallel steps

Now that we have a basic understanding of the agent’s run loop, it’s important to figure out what kinds of steps there are in such a process. In an agent workflow there are two major types: sequential and parallel.

Sequential steps

That’s when the result of step A is critical for step B. For example, in our learning app GiftGenius:

  1. First we need to understand who the gift recipient is: colleague or relative, age, interests.
  2. Then select a set of candidates via the search_gifts tool.
  3. Then filter them by budget and constraints.
  4. Then nicely format cards for the widget.
  5. And only then possibly offer a checkout handoff.

Each next step depends on data from the previous one, so execution is strictly sequential.

In pseudocode for agent behaviour, this might look like the model’s “internal plan”:

1. Ask the user about the recipient and budget
2. Call tool search_gifts(profile, budget)
3. Call tool filter_by_constraints(gifts, constraints)
4. Form the final list and descriptions

The model doesn’t literally write such a list in code, but we can nudge it toward a similar structure via system instructions, example dialogues, and tool descriptions.

Parallel steps

Sometimes steps can be performed independently. For example, we want to compare gift offers from three stores at once:

  • search_gifts_amazon
  • search_gifts_etsy
  • search_gifts_local_store

From the agent’s point of view, these are three independent tool calls that can be launched in parallel to reduce total response time.

In the Agents SDK (and modern agent frameworks in general), there’s often built-in support for parallel tool calls if the model proposes multiple calls in a single step. The canonical scenario: the model’s response describes a list of such calls, the SDK invokes them concurrently, collects the results, and injects them as a set of tool messages into the next model step.

From a planning standpoint, it looks like this:

// Agent step: the model decided to call three tools
const calls = [
  { name: "search_gifts_amazon", args: {...} },
  { name: "search_gifts_etsy", args: {...} },
  { name: "search_gifts_local_store", args: {...} },
];

const results = await Promise.all(
  calls.map(c => callTool(c.name, c.args))
);

// Then all results are added to the context before the model's next step

If you’ve written frontend code in JS/TS, you’ve already encountered the idea of parallel requests — for example, when you use Promise.all to run several fetch() calls at the same time. Now the same idea appears inside the agent’s run loop, except the decision what exactly can be done in parallel is largely made by the model itself.

4. A workflow example for GiftGenius: steps, goals, and tools

In the section on sequential steps, we already intuitively split GiftGenius behaviour into stages. Now let’s formalize this same multi-step scenario as an agent workflow: we’ll describe the goal and steps and bind them to tools and agent configuration. We won’t tie this to a specific Agents SDK API yet; instead we’ll outline the structure and add a bit of illustrative TypeScript to make it concrete.

Goal

Let the goal be:

Help the user pick 3–5 gift options for a specific recipient, considering budget, occasions, and delivery constraints, and output a structured list of gift cards for the GiftGenius widget.

Main steps

We’ll outline a minimal 4-step version:

  1. Clarify the recipient’s context
    Goal: gather information about who the gift is for (age, gender, interests, relationship to the giver), as well as budget and event date.
    Tools: possibly none — just model ↔ user dialogue.
  2. Search and initial selection
    Goal: obtain a “raw” set of gift options.
    Tools: search_gifts(profile, budget) — a tool that queries our catalog/search system and returns a list of candidates.
  3. Filtering and sorting
    Goal: exclude unsuitable options (no delivery to the region, over budget, unacceptable constraints) and sort by relevance.
    Tools: filter_and_score_gifts(candidates, constraints) — a clean, idempotent tool.
  4. Formatting the result for the widget
    Goal: bring the data to a UI-friendly format: title, short description, image, price, CTA.
    Tools: format_gift_cards(gifts) — could be a code tool (structure generation) or an LLM tool (aesthetic copy).

What this might look like in an agent configuration

Imagine we have a hypothetical agent builder (pseudocode):

import { createAgent } from "@acme/agents-sdk";
import { tools } from "./gift-tools";

export const giftAgent = createAgent({
  name: "gift-guru",
  system: `
    You are the GiftGenius agent; you help select gifts.
    Goal: propose 3–5 options that can actually be purchased,
    considering the recipient profile, budget, and delivery constraints.
    First clarify important details, then use search and filtering tools.
    Do not call tools if you still don't know the budget or key interests.
    Finish when you have a clear list of gift cards.
  `,
  tools, // this will include search_gifts, filter_and_score_gifts, format_gift_cards
  maxSteps: 12,
  timeoutMs: 15000,
});

Note a few details:

  • In the system instruction, we explicitly say the agent should clarify details first and only then call search tools. This reduces the risk that the model will start hitting tools with overly vague context.
  • We limited maxSteps so the agent doesn’t get stuck in endless loops.
  • The timeoutMs is needed so the entire run doesn’t take half of the user’s life.

5. Model auto-orchestration: what to “leave to the model” vs. what to hard-wire

An agent is a balance between model freedom and the rigid structure you design.

If you give the model too much freedom and no boundaries, you get “creative chaos”: extra tool calls, repeated steps, non-obvious loops. If you hard-code everything in the backend as a finite-state machine, the model becomes a text decorator rather than a smart task executor.

What is usually left to the model

In the context of GiftGenius and similar scenarios, it makes sense to trust the model with:

  • formulating questions to the user (how to clarify interests, how to politely ask about budget);
  • deciding when there is enough information to start searching;
  • choosing which specific tools to use within a given phase (e.g., which store search tool to use if there are several);
  • generating descriptive copy, explanations, and comparisons.

What is better to hard-code

At the same time, it’s worth fixing in advance:

  • the major scenario phases (“Information gathering” → “Search” → “Filtering” → “Formatting” → “Final”);
  • step and time limits;
  • conditions under which the agent must “stop” and explicitly tell the user the task is not feasible (for example, if the budget is 5 dollars but a pricey electronic gadget is needed for delivery tomorrow);
  • tool idempotency policy and retry strategies.

Hybrid example: phases as state, details left to the model

You can add a phase field to the agent’s state that takes values "collect_profile" | "search" | "filter" | "format" | "done". Then your backend (or the Agents SDK itself, if it supports a custom state machine) controls which tools are available in which phase.

Pseudocode:

type Phase = "collect_profile" | "search" | "filter" | "format" | "done";

interface GiftAgentState {
  phase: Phase;
  profile?: UserProfile;
  candidates?: GiftCandidate[];
  finalGifts?: GiftCard[];
}

The system instruction for the agent can include a brief description of phases, while in code you restrict the list of tools shown to the model depending on the current phase. This is an example of tool gating, which is covered in more detail in the workflow module.

6. Controlling infinite loops and useless repeats

If you give an agent an uncontrolled run loop, it will eventually behave like a student before a deadline: endlessly “clarifying and rewriting” to avoid submitting the work. Our job is to keep it from getting stuck.

There are three common sources of infinite loops:

  1. The model is unsure of the answer and keeps rephrasing the same tool request with minor changes.
  2. A tool consistently returns an error or an empty result, and the agent stubbornly tries to “give it another shot.”
  3. The agent is stuck between two tools, calling one and then the other without moving toward a final answer.

Step limit (maxSteps)

The simplest and mandatory mechanism is to limit the number of steps. In most Agents SDKs, you can specify maxSteps when starting a run or in the agent configuration. As soon as the limit is reached, the SDK ends the run with a special status (for example, aborted_by_max_steps). You then decide how to display that to the user.

In GiftGenius, we can assume that a reasonable gift selection fits in ~10 steps (a couple of clarifications, a couple of searches, filtering, formatting). We set, say, 12–15 steps with some slack and handle the situation when the limit is reached:

const run = await giftAgent.run({
  input: userGoal,
  maxSteps: 12, // override default
});

if (run.status === "max_steps_exceeded") {
  // Show an honest message to the user
}

Time limit (timeout)

Sometimes the problem isn’t the number of steps but the total duration. Tools can be slow, the network unstable. That’s why it’s useful to specify timeoutMs both at the level of an individual tool call and for the entire run.

For example, you might decide that:

  • each external API call (searching for gifts with a partner) should take no more than 3–5 seconds;
  • the entire gift-selection run should fit within 15 seconds.

If the timeout triggers, you gracefully finish the run, possibly showing the user a partial result and an honest explanation that “some sources didn’t respond in time.”

Detecting repeats

A more advanced (but useful) pattern is to detect repeated tool calls with identical arguments. If you see the agent has called search_gifts(profile, budget) three times in a row with the same parameters, that’s a sign it’s stuck.

You can add to the state a counter of calls keyed by (toolName, argsHash) and, if the counter exceeds a threshold, either:

  • abort the run and return a clear error to the user;
  • or feed the model an additional instruction: “you’ve tried to call this tool three times with the same parameters; try changing your strategy or ask the user.”

Pseudocode:

function shouldAbortToolCall(toolName: string, args: unknown, state: GiftAgentState) {
  const key = `${toolName}:${hashArgs(args)}`;
  const count = state.toolCallCounts[key] ?? 0;

  if (count >= 3) return true;

  state.toolCallCounts[key] = count + 1;
  return false;
}

Where hashArgs is any deterministic argument serialization function (for example, JSON.stringify with key sorting).

7. Clear task completion criteria

One of the key differences between a “toy” agent and a production agent is the presence of clear completion criteria. Without them, the model may either stop too early (“well, here are some gifts; you can figure out the rest”) or keep improving the result indefinitely.

In GiftGenius, you can form a simple rule:

  • The agent finishes when it has between 3 and 5 gifts with the fields: id, title, shortDescription, price, imageUrl, purchaseUrl, and they have passed filtering by budget and delivery.
  • If after at most N search and filtering attempts there are fewer than 3 suitable gifts, the agent honestly tells the user nothing decent could be found and suggests increasing the budget or relaxing the constraints.

You can encode these criteria directly in the agent’s system instruction and/or in a result check after the run.

Example of checking the result after a run:

if (run.status === "completed") {
  const gifts = run.output.gifts; // suppose our agent returns structured JSON

  if (!gifts || gifts.length < 3) {
    // The agent "completed", but the result is weak — you can:
    // 1) show an honest explanation,
    // 2) suggest that the user change the conditions.
  } else {
    // All good — show the gift widget
  }
}

Don’t expect the model to magically understand business success. As a developer, you must explicitly formulate the conditions for a “satisfactory” result and verify them.

8. Where orchestration is implemented: agent, backend, widget

We mentioned earlier that orchestration can live at different levels: in the agent, in the backend, in the widget.

From the standpoint of multi-step processes, the logic roughly breaks down as follows.

Agent (Agents SDK) is responsible for the “thoughtful” workflow:

  • how to break the goal into steps;
  • which tools to call and in what order;
  • which additional questions to ask the user.

Backend typically provides:

  • tool implementations (search, filter, commerce, etc.);
  • state storage and checkpoints;
  • hard business constraints (budget caps, permissions, regional availability).

Widget (Apps SDK) manages:

  • progress display (stepper, progress bar, “step 2 of 4”);
  • input forms;
  • UX details such as disabling buttons until all required data is provided.

A good mental model is: the agent orchestrates tools and dialogue, while the UI widget orchestrates the user’s visual experience. They communicate via structured data (ToolOutput, agent run output).

9. Mini code example: launching the multi-step GiftGenius agent from an MCP tool

Now, as promised at the start of the lecture, let’s connect the new concept with the Apps SDK → MCP → backend stack you’re familiar with and show a small example of how an MCP tool can launch an agent run.

Imagine that in your app/mcp/route.ts you have a run_gift_workflow tool that:

  • accepts a user’s text request (their goal);
  • starts the giftAgent;
  • returns a structured result for the widget.

The code is simplified and illustrative, but it shows the integration:

// app/mcp/route.ts
import { server } from "@modelcontextprotocol/sdk/server";
import { z } from "zod";
import { giftAgent } from "@/agents/giftAgent";

server.registerTool(
  "run_gift_workflow",
  {
    title: "Select gifts",
    description: "Launches a multi-step gift selection agent",
    inputSchema: {
      userGoal: z
        .string()
        .describe("The user's task, e.g., I want a gift for a colleague up to $50"),
    },
  },
  async ({ userGoal }) => { 		
    const run = await giftAgent.run({		// here we start the agent with 12 steps and a 15 sec timeout
      input: userGoal,
      maxSteps: 12,
      timeoutMs: 15000,
    });

    return {
      status: run.status,
      gifts: run.output?.gifts ?? [],
      debug: run.debugInfo, // can be removed later
    };
  }
);

Then the ChatGPT App can call this MCP tool like any other, and your GiftGenius widget can build the UI based on gifts. You get a multi-step workflow “under the hood,” while externally, for ChatGPT, it still looks like a neat single tool.

10. Common mistakes when designing multi-step processes

Mistake #1: “Let the model figure it out; I’ll just give it all the tools.”
When an agent has a dozen semantically overlapping tools without a clear system instruction and phases, the model starts flailing: calling the same thing different ways, duplicating requests, getting into loops. It’s better to spend time on design: split the scenario into phases, limit the tool list within each phase, and explicitly write the strategy in the system prompt.

Mistake #2: No step and time limits.
If you don’t set maxSteps and timeout, in production you’ll quickly get “wandering” runs that consume resources while users see nothing. Limits aren’t “optional”; they’re basic hygiene. It’s also important to handle limit exceedance meaningfully, not just fail with a silent 500.

Mistake #3: No explicit completion criteria.
The model ends the run when it thinks it’s “enough,” but its notion of “enough” can be far from business requirements. If you don’t formalize success criteria (how many gifts, which fields, which filters passed) and check them, you’ll get unstable UX: five great options today, one mediocre one plus three duplicates tomorrow.

Mistake #4: Not tracking repeated tool calls.
An agent can get stuck in a pattern of “got an error → rephrased the request by two words → called the same tool again.” If you don’t track repeated calls by (toolName, args), these cycles remain invisible until you look at logs and get shocked. Simple counters and argument hashes help a lot.

Mistake #5: Mixing orchestration and business logic implementation in a single tool.
Sometimes people try to hide an entire workflow inside one MCP tool or agent function: search, filter, formatting, and decision-making. As a result, the agent loses its point — the model cannot control the process step by step, and you lose transparency and the ability to reuse scenario parts. It’s better to extract individual stages into separate tools and give the agent their composition.

Mistake #6: No link to state and checkpoints.
A multi-step process without saving intermediate state and checkpoints becomes a brittle monolith: if something fails halfway, the user has to start from scratch. This is especially critical in scenarios where the user moves back and forth between steps or returns after some time. Use a state store, keep the phase, profile, and candidates, and let the agent resume from the right place.

Mistake #7: Ignoring the UX layer.
Developers sometimes get carried away with the agent’s internal workflow and forget that the user only sees the widget and chat messages. If the UI doesn’t show clear progress, statuses like “searching for gifts…,” “filtering options…,” the user will think the app “hung” or “isn’t doing anything,” even if the agent is orchestrating a complex process. When planning a multi-step run, think right away about how it will surface in the interface.

1
Task
ChatGPT Apps, level 12, lesson 3
Locked
Run-cycle limits (maxSteps + timeoutMs) using a "counter" example
Run-cycle limits (maxSteps + timeoutMs) using a "counter" example
1
Task
ChatGPT Apps, level 12, lesson 3
Locked
Parallel steps (Promise.all) — auto-compare 2 jets
Parallel steps (Promise.all) — auto-compare 2 jets
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION