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.
@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 stateThe crew as a function
answer_crew builds the clerk and writer. A new crew per ticket keeps runs apart.
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.
@listen("status")
def check_status(self):
result = answer_crew().kickoff(inputs={"question": self.state.message})
self.state.reply = result.rawView the code here
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."
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.
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)Dear customer, A17 shipped on 3 March. Dear customer, B22 is not an order we have. A manager will review the refund for A17.
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 alone | A crew in a flow | |
|---|---|---|
| Decides the path | The crew's process | Your flow's Python |
| Runs a model | For every task | Only on the paths that call the crew |
| Holds the state | The crew output | The 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.
Related
- Previous: Router: choosing a path in a flow
- Next: Human feedback: pausing a flow
- Reference: Flows
- Print
result.tasks_output[0].rawinsidecheck_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.