Skip to main content

Sequential Tool Orchestration: Chaining Agent Actions

Not all tools are independent. Often, tool B needs the output from tool A, and tool C needs results from both A and B. Sequential orchestration is the art of coordinating dependent tool calls so that each tool executes when its inputs are available and its outputs feed into downstream steps. The model naturally expresses this by calling one tool, reading the result, then calling the next. Your job is to build clear state management so the model understands what data is available at each step and can reason about the remaining work.

What Is Sequential Tool Orchestration?

Sequential tool orchestration is the controlled execution of dependent tools in order, with each tool's output becoming input to the next. Unlike parallel execution (which assumes independence), sequential execution assumes a directed acyclic graph (DAG) of dependencies. The model follows the DAG: call A → read result → call B → read result → call C.

Example: A customer service agent processes a refund request. The flow is: (1) look up the order in the database, (2) verify the refund policy for that product, (3) create a credit in the customer's account, (4) send a confirmation email. These must happen in order because step 3 needs the order details from step 1, and step 4 needs the confirmation number from step 3.

Sequential orchestration is not a limitation—it is the correct execution model for dependent work. Modern agent frameworks handle this naturally: the model calls a tool, sees the result, and calls the next tool based on what it learned.

The Agent Loop for Sequential Workflows

The loop is simple:

1. Model receives user request + context
2. Model calls tool A (or multiple independent tools)
3. User appends tool result(s) to messages
4. Model reads result(s), decides next action
5. Model calls tool B (or more tools)
6. Repeat until done

Each iteration adds one layer of tool calls and their results to the conversation. The conversation history acts as the state. Here is pseudocode:

messages = [{"role": "user", "content": user_request}]

while True:
# Ask model what to do next
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)

# If the model is done, break
if response.stop_reason == "end_turn":
break

# Execute all tool calls in this response
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})

# Append model's response and tool results to history
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})

This loop naturally implements sequential orchestration: the model sees the result of the last tool call before deciding what to call next. If the model needs to call 5 tools, it might call 2 in iteration 1, then 1 in iteration 2, then 2 more in iteration 3—depending on dependencies and what it learned.

Explicit State Management for Complex Workflows

For complex workflows with many steps and conditional branches, explicitly track state. This helps the model and makes debugging easier:

from dataclasses import dataclass
from enum import Enum
from typing import Dict, Any

class WorkflowState(Enum):
PENDING = "pending"
FETCHING_ORDER = "fetching_order"
VALIDATING_REFUND = "validating_refund"
PROCESSING_CREDIT = "processing_credit"
SENDING_EMAIL = "sending_email"
COMPLETE = "complete"
FAILED = "failed"

@dataclass
class WorkflowContext:
"""Holds state throughout the workflow."""
state: WorkflowState
order_id: str
order_details: Dict[str, Any] = None
refund_eligible: bool = False
credit_amount: float = 0.0
confirmation_number: str = None
email_sent: bool = False
error: str = None

def orchestrate_refund_workflow(order_id: str, messages):
"""Orchestrate a multi-step refund approval workflow."""

context = WorkflowContext(state=WorkflowState.PENDING, order_id=order_id)

# Build system prompt that includes current state
def get_system_prompt():
return f"""You are a refund agent. Current workflow state: {context.state.value}.
Completed steps: {', '.join([s.value for s in WorkflowState if s.value <= context.state.value])}.
Current data: order_id={context.order_id}, refund_eligible={context.refund_eligible}.
Next action: {get_next_action(context)}."""

while context.state != WorkflowState.COMPLETE and context.state != WorkflowState.FAILED:
messages.append({"role": "system", "content": get_system_prompt()})

response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=512,
tools=tools,
messages=messages,
system=get_system_prompt()
)

if response.stop_reason == "end_turn":
context.state = WorkflowState.COMPLETE
break

# Process tool calls and update state
for block in response.content:
if block.type == "tool_use":
if block.name == "fetch_order":
context.order_details = execute_tool(block.name, block.input)
context.state = WorkflowState.VALIDATING_REFUND
elif block.name == "check_refund_policy":
policy = execute_tool(block.name, block.input)
context.refund_eligible = policy["eligible"]
if not context.refund_eligible:
context.state = WorkflowState.FAILED
else:
context.state = WorkflowState.PROCESSING_CREDIT
elif block.name == "process_credit":
credit = execute_tool(block.name, block.input)
context.confirmation_number = credit["confirmation_id"]
context.credit_amount = credit["amount"]
context.state = WorkflowState.SENDING_EMAIL

# Append to history
messages.append({"role": "assistant", "content": response.content})
tool_results = [build_tool_result(block, context) for block in response.content if block.type == "tool_use"]
messages.append({"role": "user", "content": tool_results})

return context

This approach makes the workflow explicit: state transitions are visible, the model receives clear prompts about what step it is on, and debugging is straightforward (log the context at each step).

Patterns for Sequential Workflows

Pattern 1: Linear sequence. A → B → C → D. Each tool's output is used by the next:

# Model calls A
result_a = execute_tool(A, args_a)
# Model reads result_a, calls B with data from result_a
result_b = execute_tool(B, args_b_using_result_a)
# Model reads result_b, calls C
# ... and so on

Pattern 2: Branching. Conditional logic based on results:

# Model calls A
result_a = execute_tool(A, args)
# Model reads result_a
if result_a["status"] == "approved":
# Call B
execute_tool(B, args_b)
else:
# Call C (different path)
execute_tool(C, args_c)

Pattern 3: Aggregation. Multiple sequential chains converge:

# Call A1, A2, A3 (independent)
# Call B (depends on A1 + A2 + A3 results)
# Call C (depends on B)

Pattern 4: Looping. Repeat until a condition:

while not done:
Call A (check condition)
if A says "done", break
else, Call B (progress), go back to top

Handling Intermediate Results

Store intermediate results explicitly in the conversation context so the model can reference them:

# After each tool call, append a summary to context
context = {
"order": {...},
"refund_policy": {...},
"confirmation_number": "CNF-12345"
}

# In system prompt or conversation, reference context
system_prompt = f"""Current workflow context:
Order: {json.dumps(context['order'], indent=2)}
Refund policy check: {context['refund_policy']}
Confirmation number: {context.get('confirmation_number', 'Not yet generated')}

What is the next step?"""

The model reads the full context and can reason about what has been done and what remains.

Key Takeaways

  • Sequential orchestration chains dependent tools by having the model call one, read the result, then call the next.
  • The agent loop naturally implements sequential execution: each response drives the next action.
  • For complex workflows, use explicit state management and context to guide the model through steps.
  • Store intermediate results in the conversation history so the model can reference them.
  • Branching, looping, and aggregation patterns handle common workflow shapes.

Frequently Asked Questions

How do I prevent infinite loops in sequential workflows?

Set a maximum number of iterations (e.g., max_iterations=10) and break if exceeded. Also, use explicit state tracking: if the state does not change after a tool call, the workflow is stuck and should terminate.

Can I mix sequential and parallel execution?

Yes. Some tools can run in parallel (independent), and some must be sequential (dependent). Group by dependency level and parallelize where possible, then sequence across levels.

How do I handle errors in the middle of a workflow?

Catch tool execution errors, append the error as a tool_result, and let the model decide. The model might retry, skip that step, or abort the workflow. For critical errors, set a flag in the context so the model knows the workflow is in an error state.

Should I use explicit state machines or let the model figure it out?

For simple workflows (2–3 steps), the model is fine. For complex workflows (10+ steps, conditional branches, loops), use explicit state machines. The model works better with clear guidance and less ambiguity.

How do I debug multi-step workflows?

Log the conversation history, context state, tool calls, and results at each iteration. Use a tool trace: print which tool was called, what arguments, what result returned. This makes it easy to spot where things went wrong.

Further Reading