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
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
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)Output
Dear customer, A17 shipped on 3 March. Dear customer, B22 is not an order we have. Dear customer, which order is this about?
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
| Ticket | Router label | What answered |
|---|---|---|
| Where is my order A17? | status | The crew, from the tool |
| Where is my order B22? | status | The crew, passing on not-found |
| Can you help me? | no_order | The 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.
Related
- Previous: Desk crew inside a flow
- Next: Desk refund: pausing for a manager
- Reference: CrewAI docs, Flows
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.stateafter each run and watch the fields fill in.
Slow is fine. Stopping is the only problem.