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

Choosing a path with @router

A @router method returns a label, and every method listening to that label runs next. It is how a flow takes one path for refunds and another for order status.

Lesson 21's desk treated every ticket the same. A refund and a status question need different handling.

Exampledesk_flow.py, from lesson 21
import re

import shop_llm
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel


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

The state and imports are lesson 21's. The router needs one more import, and the Desk class gets a second decorator.

Exampledesk_flow.py, one more import
from crewai.flow.flow import router
Exampledesk_flow.py, the Desk class
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 refund(self):
        self.state.reply = f"A manager will review the refund for {self.state.order_id}."

The mistake shows up when the flow is created, not when the file is read:

Example
desk = Desk()

@router works like @listen, but the method's return value is a label, and @listen("refund") waits for that label. The flow was refused as soon as it was created: the method that listens to "refund" is also called refund, and a method's own name counts as a trigger, so it would run itself forever.

Distinct names

Exampledesk_flow.py, the end of the Desk class
    @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):
        self.state.reply = f"Looking into {self.state.order_id}."

The handlers are now hold_refund and check_status, and there is one for each label the router can return.

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

The router returned one label per ticket, and only the method listening to it ran. A label no method listens to ends the flow there, which is easy to do by a typo.

One router, two paths
statusrefundkickoff(inputs)one messageTicket statemessage, order_idread_ticket@start()route@routercheck_status@listen("status")hold_refund@listen("refund")
Hover or tap a piece to see what it is and which lesson built it.
Trace two tickets

Pick one to watch it run, step by step.

Both tickets take the same two steps first, and split only at the router. The state is what the router reads, and what every method writes to.

Try it yourself
  • Add a label "no_order" for messages without an order id, and a handler for it.
  • Return "Refund" with a capital R and see what runs.
  • Import or_ and add a method that listens to or_("refund", "status").

You understood something today that you didn't yesterday.