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 →

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.

python
@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.

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

The two outcomes

propose and decide are above; two handlers carry out the manager's answer, one per label decide can return.

python
    @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."
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
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)
        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.

Example
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

What the pause did

  • The provider printed the proposed refund, then raised HumanFeedbackPending.
  • kickoff returned a HumanFeedbackPending object instead of a result, and the flow's state went into refunds.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.

Example
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"))

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 actsBefore a tool runsAfter a method runs
What it waits forNothing; it allows or blocksA person's answer
Across program runsNoYes, 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.
Watch out. A flow that pauses needs a persistence store, or its state is lost when the program ends. @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.
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.