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

The crew and the flow around it

The desk answers order questions with a crew of two agents, and decides which tickets reach it with a router.

The crew is the clerk and the writer, with the guardrail on the reply task so no email with a card number leaves it.

Exampledesk.py, continued
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,
                 guardrail=no_card_numbers)
    return Crew(agents=[clerk, writer], tasks=[look, reply])

The clerk has the lookup tool and answers with the order's status. The writer turns that into an email the customer can read.

Exampledesk.py, continued
class SupportDesk(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):
        if not self.state.order_id:
            return "no_order"
        return "refund" if "refund" in self.state.message.lower() else "status"

The flow starts by reading the ticket for an order id. The router then sends it one of three ways: a refund, a status question, or a ticket with no order in it at all, which never reaches a model.

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

    @listen("no_order")
    def ask_for_order(self):
        self.state.reply = "Dear customer, which order is this about?"

The status path runs the crew and keeps its reply. The no-order path answers without one.

Exampledesk.py, in SupportDesk
    @human_feedback(message="Approve this refund?", provider=ManagerInbox())
    @listen("refund")
    def propose_refund(self):
        return f"Refund {self.state.order_id} in full"

    @router(propose_refund)
    def decide(self, result):
        return "approved" if result.feedback.lower().startswith("yes") else "refused"

A refund is proposed, then paused: @human_feedback hands it to ManagerInbox and stops the flow until someone answers. The second router reads that answer.

Exampledesk.py, in SupportDesk
    @listen("approved")
    def pay(self):
        self.state.reply = f"Dear customer, your refund for {self.state.order_id} is on its way."

    @listen("refused")
    def refuse(self):
        self.state.reply = f"Dear customer, a manager could not approve a refund for {self.state.order_id}."

Whatever the manager said, the customer gets a reply. Nothing is paid without a yes.

The finished support desk
SupportDesk flowanswer_crew()statusrefundwaitsresumeread_ticketfinds the order idroutestatus, refund, no idpropose_refund@human_feedbackdecideapproved or refusedask_for_orderno order idOrder clerkcalls lookup_orderReply writerguardrail checks itThe shop's ordersA17, C40, not B22ManagerInboxpauses and savesdesk.dbthe paused flowstate.replywhat the customer reads
Hover or tap a piece to see what it is and which lesson built it.
Trace a ticket

Pick one to watch it run, step by step.

The whole desk, with every path a ticket can take. Click through the four traces: a status question, an order that does not exist, a refund approved and a refund refused.

Try it yourself
  • Add a "cancel" label to the router with its own handler.
  • Give the clerk the MCP tool in place of lookup_order.
  • Move the guardrail to the clerk's task and see which text it checks.

This is what real progress feels like.