CrewAICrewAI 1.15 · Python 3.10 to 3.13
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
35 small wins to finish your pathNext lesson →

Sequential tasks: two agents in a row

A sequential crew runs its tasks in list order and hands each task the output of the ones before it.

Last updated: 28 Sep, 2026 · CrewAI 1.15

A support desk has two jobs: a clerk who finds an order, and a writer who answers the customer from what the clerk found. Two agents and two tasks in one crew do exactly that, and the writer reads the clerk's result without you wiring anything.

The sequential process

A crew's default process is sequential: tasks run top to bottom, and every later task receives the earlier outputs as context.

python
crew = Crew(agents=[clerk, writer], tasks=[look, reply])  # process="sequential" by default
reply.context = [look]   # optional: name exactly which task outputs this task sees

The two agents

The clerk carries the lookup tool. The writer has none: its job is words, built from what the clerk hands over.

python
clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
              backstory="You can look up any order in the shop's system.",
              llm=ShopLLM(model="shop"), tools=[lookup_order])
writer = Agent(role="Reply writer", goal="Write replies to customers",
               backstory="You write short, friendly emails.",
               llm=ShopLLM(model="shop"))

The two tasks and the crew

The reply task never mentions the question. It works from the clerk's answer, because the crew passes it forward.

python
look = Task(description="Find the order in this message: {question}",
            expected_output="The order's status.", agent=clerk)
reply = Task(description="Write the customer a reply.",
             expected_output="A short, friendly email.", agent=writer)
crew = Crew(agents=[clerk, writer], tasks=[look, reply], verbose=False)
Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports these files. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep them in the same folder.
View the code here
tools.py
from crewai.tools import tool

ORDERS = {"A17": "shipped on 3 March", "C40": "waiting for stock"}


@tool
def lookup_order(order_id: str) -> str:
    """Look up an order's shipping status by its id, such as A17."""
    status = ORDERS.get(order_id)
    return f"{order_id} {status}." if status else f"{order_id} is not an order we have."
shop_llm.py
import json
import os
import re

from crewai import BaseLLM

os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
os.environ["CREWAI_TRACING_ENABLED"] = "false"
os.environ["CREWAI_DISABLE_VERSION_CHECK"] = "true"


class ShopLLM(BaseLLM):
    script: list = []

    def supports_function_calling(self):
        return True

    def call(self, messages, tools=None, **kwargs):
        if isinstance(messages, str):
            messages = [{"role": "user", "content": messages}]
        if self.script:
            return self.script.pop(0)
        names = [t["function"]["name"] for t in tools or []]
        return self.decide(messages, names)

    def decide(self, messages, tools):
        last = messages[-1]
        if last["role"] == "tool":
            return last["content"]
        text = last["content"]
        orders = re.findall(r"\b[A-Z]\d+\b", text)
        wanted = "refund_order" if "refund" in text.lower() else "lookup_order"
        matches = [name for name in tools if name.endswith(wanted)]
        if orders and matches:
            args = json.dumps({"order_id": orders[0]})
            return [{"id": f"call_{orders[0]}", "type": "function",
                     "function": {"name": matches[0], "arguments": args}}]
        if "working with:" in text:
            context = text.split("working with:")[1].strip().split("\n\n")[0]
            return f"Dear customer, {context}"
        if orders:
            return f"I have no way to look up {orders[0]} yet."
        return "Hello. Which order is this about?"

Two tasks, one after the other

The whole program. Printing tasks_output shows both steps in order.

Example
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"

from crewai import Agent, Crew, Task
from shop_llm import ShopLLM
from tools import lookup_order

clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
              backstory="You can look up any order in the shop's system.",
              llm=ShopLLM(model="shop"), tools=[lookup_order])
writer = Agent(role="Reply writer", goal="Write replies to customers",
               backstory="You write short, friendly emails.",
               llm=ShopLLM(model="shop"))

look = Task(description="Find the order in this message: {question}",
            expected_output="The order's status.", agent=clerk)
reply = Task(description="Write the customer a reply.",
             expected_output="A short, friendly email.", agent=writer)
crew = Crew(agents=[clerk, writer], tasks=[look, reply], verbose=False)

result = crew.kickoff(inputs={"question": "Where is my order A17?"})
for output in result.tasks_output:
    print(f"{output.agent}: {output.raw}")

What the two tasks produced

  • The clerk ran first and its output is the order's status, looked up with the tool.
  • The writer ran second and built its email from the clerk's finding, which arrived as context.
  • result.raw is the last task's output, so the crew's result is the writer's email.

Choosing what a task sees

context names the tasks whose outputs a task receives. Setting reply.context = [look] here matches the default, but with three tasks it lets the last one read the first and skip the middle.

Example
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"

from crewai import Agent, Crew, Task
from shop_llm import ShopLLM
from tools import lookup_order

clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
              backstory="You can look up any order in the shop's system.",
              llm=ShopLLM(model="shop"), tools=[lookup_order])
writer = Agent(role="Reply writer", goal="Write replies to customers",
               backstory="You write short, friendly emails.",
               llm=ShopLLM(model="shop"))

look = Task(description="Find the order in this message: {question}",
            expected_output="The order's status.", agent=clerk)
reply = Task(description="Write the customer a reply.",
             expected_output="A short, friendly email.", agent=writer)
crew = Crew(agents=[clerk, writer], tasks=[look, reply], verbose=False)

reply.context = [look]
result = crew.kickoff(inputs={"question": "Where is my order B22?"})
print(result.raw)

Sequential vs hierarchical

SequentialHierarchical
Order of workThe order tasks are listedA manager decides at run time
Who a task hears fromEvery earlier task, or its contextWhatever the manager passes on
Model callsOne per taskExtra calls for the manager
Best whenThe steps are knownThe order depends on the answer

When to run tasks in a row

  • A pipeline with fixed steps: find the order, then write the reply.
  • Any job where a later step needs an earlier step's result and the order never changes.
Watch out. A later task sees earlier outputs, not the run's inputs, so if a task needs the original question set its description with a {question} placeholder as the clerk's task does. Reordering the tasks changes what each one receives.
Try it yourself
  • Swap the order of the tasks in the crew and read what the writer gets.
  • Set reply.context = [] and see which branch of decide answers.
  • Add a third task for the writer that shortens the reply.

This is what real progress feels like.