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 →

Three tickets through the desk

Three tickets take three paths through the desk: an order that exists, one that does not, and a message with no order in it, so the happy path and a not-found failure both show.

Last updated: 28 Sep, 2026 · CrewAI 1.15

The desk is finished. Now run tickets through it. Each is a fresh flow, with a database file so a paused one can be picked up later.

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
desk.py
import re
from crewai import Agent, Crew, Task
from crewai.flow import Flow, HumanFeedbackPending, HumanFeedbackProvider, human_feedback
from crewai.flow.flow import 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 no_card_numbers(output):
    if re.search(r"\d{4} ?\d{4} ?\d{4} ?\d{4}", output.raw):
        return (False, "Remove the card number. Never repeat one to a customer.")
    return (True, output.raw)


class ManagerInbox(HumanFeedbackProvider):
    def request_feedback(self, context, flow):
        print("to the manager:", context.method_output)
        raise HumanFeedbackPending(context=context)


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], verbose=False)
    writer = Agent(role="Reply writer", goal="Write replies to customers",
                   backstory="You write short emails.", llm=ShopLLM(model="shop"),
                   verbose=False)
    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])


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"

    @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?"

    @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"

    @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}."
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)
        return self.decide(messages, tools or [])

    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)
        want_refund = "refund" in text.lower()
        chosen = None
        for t in tools:
            fn = t["function"]
            label = (fn["name"] + " " + (fn.get("description") or "")).lower()
            is_refund = "refund" in label
            if want_refund and is_refund:
                chosen = fn["name"]
                break
            if not want_refund and not is_refund and ("look up" in label or "status" in label):
                chosen = fn["name"]
                break
        if orders and chosen:
            args = json.dumps({"order_id": orders[0]})
            return [{"id": f"call_{orders[0]}", "type": "function",
                     "function": {"name": chosen, "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?"

One flow per ticket, on a database

python
from crewai.flow.persistence import SQLiteFlowPersistence
from desk import SupportDesk

db = SQLiteFlowPersistence("desk.db")

SQLiteFlowPersistence writes each flow's state to desk.db. A status ticket never pauses, but the refund lesson needs the same store, so it is set up here.

Three tickets, three paths

Examplerun_desk.py
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
from crewai.flow.persistence import SQLiteFlowPersistence
from desk import SupportDesk

db = SQLiteFlowPersistence("desk.db")

for message in ["Where is my order A17?", "Where is my order B22?", "Can you help me?"]:
    desk = SupportDesk(persistence=db, suppress_flow_events=True)
    desk.kickoff(inputs={"message": message})
    print(desk.state.reply)

Reading each path

  • A17 went through the crew: the clerk called lookup_order, the writer wrote the email, and the guardrail passed it.
  • B22 is the not-found failure: the tool reported that the order does not exist, and the desk passed that on instead of inventing a status.
  • The third ticket named no order, so the router sent it to the no-order path, which answered without calling a model.
  • One store held all three, each under its own flow id, which is what lets a paused ticket resume.

The three branches side by side

TicketRouter labelWhat answered
Where is my order A17?statusThe crew, from the tool
Where is my order B22?statusThe crew, passing on not-found
Can you help me?no_orderThe flow, with no model call

When a not-found path matters

  • A real catalogue is full of ids that do not exist, so the desk must answer them without guessing.
  • A tool that reports missing data lets the reply stay truthful.
  • A branch with no model call keeps cost down on tickets that need none.
Watch out
A tool that returns "not found" is a success to the crew, not an error, so the reply carries it straight through. If a missing order should be handled differently, branch on the tool's result in the flow; do not expect the crew to raise.
Try it yourself
  • Send a ticket that mentions two orders and see which one the router picks.
  • Add a fourth ticket about a delivery date.
  • Print desk.state after each run and watch the fields fill in.

Slow is fine. Stopping is the only problem.