Human feedback: pausing a flow
@human_feedback pauses a flow after a method to ask a person about its result: the flow is saved, and resume carries on later with their answer.
Last updated: 28 Sep, 2026 · CrewAI 1.15
A refund should wait for a manager. A flow can stop, send the proposed refund out for approval, and wait, then continue when the reply arrives, even in a later program run.
The human_feedback decorator
Decorate a method with @human_feedback. A provider chooses where the question goes; raising HumanFeedbackPending tells the flow to save itself and stop.
@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"A provider that messages a manager
ManagerInbox prints the question, standing in for a message to a manager, then raises HumanFeedbackPending.
class ManagerInbox(HumanFeedbackProvider):
def request_feedback(self, context, flow):
print("to the manager:", context.method_output)
raise HumanFeedbackPending(context=context) # save and stopThe two outcomes
propose and decide are above; two handlers carry out the manager's answer, one per label decide can return.
@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."View the code here
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."
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)
names = [t["function"]["name"] for t in tools or []]
return self.decide(messages, names)
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)
wanted = "refund_order" if "refund" in text.lower() else "lookup_order"
matches = [name for name in tools if name.endswith(wanted)]
if orders and matches:
args = json.dumps({"order_id": orders[0]})
return [{"id": f"call_{orders[0]}", "type": "function",
"function": {"name": matches[0], "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 flow that pauses for approval
Kick it off with a persistence store. The flow pauses instead of finishing.
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
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)
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"
@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."
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
to the manager: Refund A17 in full ╭─────────────────────────────── ⏳ Flow Paused ───────────────────────────────╮ │ │ │ Flow Paused - Waiting for Feedback │ │ Name: Refund │ │ ID: ef395842-79c4-45a4-9b85-1170c15ce0f9 │ │ │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ HumanFeedbackPending
What the pause did
- The provider printed the proposed refund, then raised
HumanFeedbackPending. - kickoff returned a
HumanFeedbackPendingobject instead of a result, and the flow's state went intorefunds.db. - The paused panel prints even with
suppress_flow_events, and your program can end here.
Resuming with the answer
Later, load the saved flow by its id and hand over the manager's answer.
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true"
later = Refund.from_pending(flow_id, db, suppress_flow_events=True)
print(later.resume("Yes, go ahead"))Refund for A17 sent.
from_pending loads the saved flow by its id, and resume passes the manager's answer. The flow continued from decide with the state it had, so it still knew the order was A17.
Pausing a flow vs blocking a tool
| A tool hook | @human_feedback | |
|---|---|---|
| When it acts | Before a tool runs | After a method runs |
| What it waits for | Nothing; it allows or blocks | A person's answer |
| Across program runs | No | Yes, via a persistence store |
When to wait for a person
- An action needs sign-off: a refund, a send, a delete.
- The wait may be long, so the flow must survive the program ending and restart from its saved state.
@human_feedback can also take an llm to map free-form feedback to an outcome, but that model is saved by name and rebuilt on resume, which a stand-in class cannot survive, so this lesson routes on the words itself.Related
- Previous: Crew in a flow: one step of a flow
- Next: Streaming a crew's output
- Reference: Human feedback in flows
- Resume with "No, the parcel was delivered" and read the result.
- Print
later.statebefore callingresume. - Make
ManagerInboxreturn"yes"instead of raising, and run the flow in one go.
This is what real progress feels like.