Skip to main content

Overview

The @human_feedback decorator requires CrewAI version 1.8.0 or higher. Make sure to update your installation before using this feature.
The @human_feedback decorator enables human-in-the-loop (HITL) workflows directly within CrewAI Flows. It allows you to pause flow execution, present output to a human for review, collect their feedback, and optionally route to different listeners based on the feedback outcome. This is particularly valuable for:
  • Quality assurance: Review AI-generated content before it’s used downstream
  • Decision gates: Let humans make critical decisions in automated workflows
  • Approval workflows: Implement approve/reject/revise patterns
  • Interactive refinement: Collect feedback to improve outputs iteratively

Quick Start

Here’s the simplest way to add human feedback to a flow:
Code
When this flow runs, it will:
  1. Execute generate_content and return the string
  2. Display the output to the user with the request message
  3. Wait for the user to type feedback (or press Enter to skip)
  4. Pass a HumanFeedbackResult object to process_feedback

The @human_feedback Decorator

Parameters

Basic Usage (No Routing)

When you don’t specify emit, the decorator simply collects feedback and passes a HumanFeedbackResult to the next listener:
Code

Routing with emit

When you specify emit, the decorator becomes a router. The human’s free-form feedback is interpreted by an LLM and collapsed into one of the specified outcomes:
Code
When the human says something like “needs more detail”, the LLM collapses that to "needs_revision", which triggers review_content again via or_() — creating a revision loop. The loop continues until the outcome is "approved" or "rejected".
The LLM uses structured outputs (function calling) when available to guarantee the response is one of your specified outcomes. This makes routing reliable and predictable.
A @start() method only runs once at the beginning of the flow. If you need a revision loop, separate the start method from the review method and use @listen(or_("trigger", "revision_outcome")) on the review method to enable the self-loop.

HumanFeedbackResult

The HumanFeedbackResult dataclass contains all information about a human feedback interaction:
Code

Accessing in Listeners

When a listener is triggered by a @human_feedback method with emit, it receives the HumanFeedbackResult:
Code

Accessing Feedback History

The Flow class provides two attributes for accessing human feedback:

last_human_feedback

Returns the most recent HumanFeedbackResult:
Code

human_feedback_history

A list of all HumanFeedbackResult objects collected during the flow:
Code
Each HumanFeedbackResult is appended to human_feedback_history, so multiple feedback steps won’t overwrite each other. Use this list to access all feedback collected during the flow.

Complete Example: Content Approval Workflow

Here’s a full example implementing a content review and approval workflow with a revision loop:
The key pattern is @listen(or_("generate_draft", "needs_revision")) — the review method listens to both the initial trigger and its own revision outcome, creating a self-loop that repeats until the human approves or rejects.

Combining with Other Decorators

The @human_feedback decorator works with @start(), @listen(), and or_(). Both decorator orderings work — the framework propagates attributes in both directions — but the recommended patterns are:
Code

Self-loop pattern

To create a revision loop, the review method must listen to both an upstream trigger and its own revision outcome using or_():
Code
When the outcome is "revise", the flow routes back to review (because it listens to "revise" via or_()). When the outcome is "approved", the flow continues to publish. This works because the flow engine exempts routers from the “fire once” rule, allowing them to re-execute on each loop iteration.

Chained routers

A listener triggered by one router’s outcome can itself be a router:
Code

Limitations

  • @start() methods run once: A @start() method cannot self-loop. If you need a revision cycle, use a separate @start() method as the entry point and put the @human_feedback on a @listen() method.
  • No @start() + @listen() on the same method: This is a Flow framework constraint. A method is either a start point or a listener, not both.

Best Practices

1. Write Clear Request Messages

The message parameter is what the human sees. Make it actionable:
Code

2. Choose Meaningful Outcomes

When using emit, pick outcomes that map naturally to human responses:
Code

3. Always Provide a Default Outcome

Use default_outcome to handle cases where users press Enter without typing:
Code

4. Use Feedback History for Audit Trails

Access human_feedback_history to create audit logs:
Code

5. Handle Both Routed and Non-Routed Feedback

When designing flows, consider whether you need routing:

Async Human Feedback (Non-Blocking)

By default, @human_feedback blocks execution waiting for console input. For production applications, you may need async/non-blocking feedback that integrates with external systems like Slack, email, webhooks, or APIs.

The Provider Abstraction

Use the provider parameter to specify a custom feedback collection strategy:
Code
The flow framework automatically persists state when HumanFeedbackPending is raised. Your provider only needs to notify the external system and raise the exception—no manual persistence calls required.

Handling Paused Flows

When using an async provider, kickoff() returns a HumanFeedbackPending object instead of raising an exception:
Code

Resuming a Paused Flow

When feedback arrives (e.g., via webhook), resume the flow:
Code

Key Types

PendingFeedbackContext

The context contains everything needed to resume:
Code

Complete Async Flow Example

Code
If you’re using an async web framework (FastAPI, aiohttp, Slack Bolt async mode), use await flow.resume_async() instead of flow.resume(). Calling resume() from within a running event loop will raise a RuntimeError.

Best Practices for Async Feedback

  1. Check the return type: kickoff() returns HumanFeedbackPending when paused—no try/except needed
  2. Use the right resume method: Use resume() in sync code, await resume_async() in async code
  3. Store callback info: Use callback_info to store webhook URLs, ticket IDs, etc.
  4. Implement idempotency: Your resume handler should be idempotent for safety
  5. Automatic persistence: State is automatically saved when HumanFeedbackPending is raised and uses SQLiteFlowPersistence by default
  6. Custom persistence: Pass a custom persistence instance to from_pending() if needed

Learning from Feedback

The learn=True parameter enables a feedback loop between human reviewers and the memory system. When enabled, the system progressively improves its outputs by learning from past human corrections.

How It Works

  1. After feedback: The LLM extracts generalizable lessons from the output + feedback and stores them in memory with source="hitl". If the feedback is just approval (e.g. “looks good”), nothing is stored.
  2. Before next review: Past HITL lessons are recalled from memory and applied by the LLM to improve the output before the human sees it.
Over time, the human sees progressively better pre-reviewed output because each correction informs future reviews.

Example

Code
First run: The human sees the raw output and says “Always include citations for factual claims.” The lesson is distilled and stored in memory. Second run: The system recalls the citation lesson, pre-reviews the output to add citations, then shows the improved version. The human’s job shifts from “fix everything” to “catch what the system missed.”

Configuration

Key Design Decisions

  • Same LLM for everything: The llm parameter on the decorator is shared by outcome collapsing, lesson distillation, and pre-review. No need to configure multiple models.
  • Structured output: Both distillation and pre-review use function calling with Pydantic models when the LLM supports it, falling back to text parsing otherwise.
  • Non-blocking storage: Lessons are stored via remember_many() which runs in a background thread — the flow continues immediately.
  • Graceful degradation: If the LLM fails during distillation, nothing is stored. If it fails during pre-review, the raw output is shown. Neither failure blocks the flow.
  • No scope/categories needed: When storing lessons, only source is passed. The encoding pipeline infers scope, categories, and importance automatically.
learn=True requires the Flow to have memory available. Flows get memory automatically by default, but if you’ve disabled it with _skip_auto_memory, HITL learning will be silently skipped.