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

Waiting for a person

@human_feedback pauses a flow after a method and asks a person about its result. The flow is saved, and resume carries on hours later with their answer.

Lesson 12 blocked refunds until an order id was in a set, but somebody still had to put it there. A flow can stop, ask a manager, and wait for the reply.

Examplerefunds.py
import shop_llm
from crewai.flow import Flow, HumanFeedbackPending, HumanFeedbackProvider, human_feedback
from crewai.flow.flow import listen, router, start
from crewai.flow.persistence import SQLiteFlowPersistence


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

By default @human_feedback asks at the terminal and waits. A provider changes where the question goes. ManagerInbox prints it, standing in for a message to a manager, then raises HumanFeedbackPending, which tells the flow to save itself and stop.

Examplerefunds.py, continued
class Refund(Flow):
    @start()
    @human_feedback(message="Approve this refund?", provider=ManagerInbox())
    def propose(self):
        return f"Refund {self.state['order_id']} in full"

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

propose returns the refund it wants, and @human_feedback sends that to the provider. When feedback arrives, decide receives a result whose feedback is the manager's words and routes on them.

Examplerefunds.py, in the Refund class
    @listen("approved")
    def pay(self):
        return f"Refund for {self.state['order_id']} sent."

    @listen("refused")
    def refuse(self):
        return f"Refund for {self.state['order_id']} refused."
Example
db = SQLiteFlowPersistence("refunds.db")
flow = Refund(persistence=db, suppress_flow_events=True)
pending = flow.kickoff(inputs={"order_id": "A17"})
print(type(pending).__name__)
flow_id = pending.context.flow_id

kickoff returned a HumanFeedbackPending instead of a result, and the flow's state went into refunds.db, an SQLite file. The paused panel prints even with suppress_flow_events. Your program can end here.

Resuming with the answer

Example
later = Refund.from_pending(flow_id, db, suppress_flow_events=True)
print(later.resume("Yes, go ahead"))

from_pending loads the saved flow by its id, and resume hands over the manager's answer. The flow continued from decide with the state it had, so it still knew the order was A17.

@human_feedback can also take emit, a list of outcomes, and an llm that maps free-form feedback to one of them. That model is saved as a model name when the flow pauses and rebuilt from the name on resume, which a class like ShopLLM cannot survive; this lesson routes on the words itself.

Try it yourself
  • Resume with "No, the parcel was delivered" and read the result.
  • Print later.state before calling resume.
  • Make ManagerInbox return "yes" instead of raising, and run the flow in one go.

This is what real progress feels like.