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 path

Desk refund: pausing for a manager

A refund is the one thing the desk will not do on its own: the flow stops, saves itself, and waits for a manager, and a refusal is a real outcome with its own reply.

Last updated: 28 Sep, 2026 · CrewAI 1.15

The three-ticket run showed the happy path and a not-found ticket. A refund is different: it pauses. This lesson runs one, then resumes it two ways, and one of them is the desk refusing to pay.

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

A refund that stops and saves

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

db = SQLiteFlowPersistence("desk.db")
desk = SupportDesk(persistence=db, suppress_flow_events=True)
pending = desk.kickoff(inputs={"message": "Please refund order A17, it arrived broken."})
flow_id = pending.context.flow_id

The refund path reaches propose_refund, which hands the proposal to the manager's inbox and pauses. kickoff returns a HumanFeedbackPending object carrying the flow id, so the program can stop and pick the ticket up later.

The flow pausing for the manager

Examplerefund.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")
desk = SupportDesk(persistence=db, suppress_flow_events=True)
pending = desk.kickoff(inputs={"message": "Please refund order A17, it arrived broken."})
flow_id = pending.context.flow_id

The proposal reached the manager and the flow stopped there. Its state is in desk.db, so the process can end and nothing is lost. No refund has been made.

Resuming to a refusal, the failure path

Example
later = SupportDesk.from_pending(flow_id, db, suppress_flow_events=True)
later.resume("No, it was delivered intact")
print(later.state.reply)

from_pending rebuilds the flow from the saved state, and resume carries the manager's answer into the second router. An answer that does not start with "yes" takes the refused branch, so the desk tells the customer no refund was made. The flow never reaches pay, which is the whole point of the pause.

Resuming to a payment

Example
later = SupportDesk.from_pending(flow_id, db, suppress_flow_events=True)
later.resume("Yes, refund it")
print(later.state.reply)

The same saved ticket, resumed with a yes, takes the approved branch and pay writes the reply. Nothing is paid until that answer arrives.

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 two ends of a refund

RefusedApproved
Manager answersAnything not starting with "yes"Starts with "yes"
Branch takenrefusedapproved
Reaches payNoYes
Reply to customerNo refund was madeRefund is on its way

When a pause for a person belongs in a flow

  • Any action that spends money or cannot be undone, such as a refund.
  • A step that needs a person's judgement the model should not make alone.
  • Work that may sit for hours before someone answers, so the state must be saved.
Watch out
A resumed flow finishes and its pending record is spent, so resuming the same flow id twice fails. Start a fresh kickoff for each ticket, and read the outcome from state.reply after resume returns.

What this course left out

CrewAI is large. These are the parts a first course does not cover, and where to read each one.

TopicWhat it is forDocs
Task human_inputA person approves or edits a task's result after it runshuman-in-the-loop
ReasoningAn agent that thinks a problem through before it actsreasoning
PlanningA model that writes a plan into every task before the crew runsplanning
AsyncRunning crews at the same time with kickoff_async and kickoff_for_eachkickoff-async
CheckpointingSaving a flow between steps so a long run can restartcheckpointing
TrainingFeeding human corrections back so a crew improvestraining
MultimodalAgents that read images, not only textmultimodal agents
CLIcrewai create and the project layout it buildsCLI
StreamingReading a run's output as it is producedstreaming
TestingScoring a crew's runs with crewai testtesting
Try it yourself
  • Resume with "maybe" and read which branch that takes.
  • Add a rule that refunds under 20 need no approval.
  • Store each finished reply in the crew's memory, under the customer's scope.

This is what real progress feels like.