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 →

Crew in a flow: one step of a flow

A flow method can build a crew and kick it off like any other code, so the flow decides which tickets need agents and the crew handles the work that needs a model.

Last updated: 28 Sep, 2026 · CrewAI 1.15

The status path so far wrote a placeholder reply. The clerk-and-writer crew can write the real one, called from inside one flow method.

Calling a crew from a method

A method builds a crew, runs it, and stores the result in the state. Nothing new: it is the crew from part 3 inside a flow method.

python
@listen("status")
def check_status(self):
    result = answer_crew().kickoff(inputs={"question": self.state.message})
    self.state.reply = result.raw   # store the crew's answer in the flow state

The crew as a function

answer_crew builds the clerk and writer. A new crew per ticket keeps runs apart.

python
def answer_crew():
    clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
                  backstory="You can look up any order.", llm=ShopLLM(model="shop"),
                  tools=[lookup_order])
    writer = Agent(role="Reply writer", goal="Write replies to customers",
                   backstory="You write short 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)
    return Crew(agents=[clerk, writer], tasks=[look, reply], verbose=False)

The status handler

The status handler replaces the placeholder: it passes the message to the crew and stores the writer's email. The refund path still needs no model.

python
    @listen("status")
    def check_status(self):
        result = answer_crew().kickoff(inputs={"question": self.state.message})
        self.state.reply = result.raw
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?"

Three tickets through the flow

Two status questions go through the crew; the refund takes the other path.

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

import re

from crewai import Agent, Crew, Task
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
from shop_llm import ShopLLM
from tools import lookup_order


class Ticket(BaseModel):
    message: str = ""
    order_id: str = ""
    reply: str = ""


def answer_crew():
    clerk = Agent(role="Order clerk", goal="Find the status of customers' orders",
                  backstory="You can look up any order.", llm=ShopLLM(model="shop"),
                  tools=[lookup_order])
    writer = Agent(role="Reply writer", goal="Write replies to customers",
                   backstory="You write short 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)
    return Crew(agents=[clerk, writer], tasks=[look, reply], verbose=False)


class Desk(Flow[Ticket]):
    @start()
    def read_ticket(self):
        found = re.findall(r"\b[A-Z]\d+\b", self.state.message)
        self.state.order_id = found[0] if found else ""

    @router(read_ticket)
    def route(self):
        return "refund" if "refund" in self.state.message.lower() else "status"

    @listen("refund")
    def hold_refund(self):
        self.state.reply = f"A manager will review the refund for {self.state.order_id}."

    @listen("status")
    def check_status(self):
        result = answer_crew().kickoff(inputs={"question": self.state.message})
        self.state.reply = result.raw

for message in ["Where is my order A17?", "Where is my order B22?", "Please refund order A17."]:
    desk = Desk(suppress_flow_events=True)
    desk.kickoff(inputs={"message": message})
    print(desk.state.reply)

What each path produced

  • The two status tickets went through the crew and came back as emails, including the order that does not exist.
  • The refund took the other path and no model was called for it.
  • The flow owns the state and the decisions; a crew is one unit of work inside it.

A crew alone vs a crew in a flow

A crew aloneA crew in a flow
Decides the pathThe crew's processYour flow's Python
Runs a modelFor every taskOnly on the paths that call the crew
Holds the stateThe crew outputThe flow's typed state

When to wrap a crew in a flow

  • Most of the work is plain routing, with a model needed on only some paths.
  • You want one place, the flow state, that holds the whole ticket's progress.
Watch out. Building the crew inside the method makes a fresh crew per ticket, which keeps runs apart; a crew built once and shared carries state between tickets. This is the layout CrewAI's production guide recommends.
Try it yourself
  • Print result.tasks_output[0].raw inside check_status.
  • Create the crew once, outside the flow, and run all three tickets again.
  • Store the clerk's finding in a new state field status.

Slow is fine. Stopping is the only problem.