Skip to main content
Human-in-the-loop (HITL) patterns ensure critical decisions receive human review before execution. This is essential for high-stakes operations like financial transactions, content publishing, or data deletion.

When to Use Human-in-the-Loop

Add human oversight when agents:
  • Make financial transactions or purchases
  • Modify or delete production data
  • Send external communications (emails, posts)
  • Make irreversible decisions
  • Handle sensitive information
  • Require domain expertise validation

Basic Approval Pattern

Simple Yes/No Confirmation

from strands import Agent

def get_human_approval(action: str, context: dict) -> bool:
    """
    Request human approval for an action.
    
    Args:
        action: Description of the action to approve
        context: Additional context for decision
    
    Returns:
        True if approved, False if rejected
    """
    print(f"\n🔔 Human Approval Required")
    print(f"Action: {action}")
    print(f"Context: {json.dumps(context, indent=2)}")
    
    while True:
        response = input("\nApprove? (yes/no): ").lower().strip()
        if response in ['yes', 'y']:
            return True
        elif response in ['no', 'n']:
            return False
        print("Please enter 'yes' or 'no'")

# Use in agent workflow
def execute_with_approval(agent: Agent, query: str):
    # Get agent's proposed action
    result = agent(query)
    
    # Request approval
    if get_human_approval(
        action="Execute agent recommendation",
        context={"query": query, "recommendation": result}
    ):
        # Execute the action
        return execute_action(result)
    else:
        return "Action cancelled by user"

AWS Strands HITL Pattern

From course/aws_strands/05_human_in_the_loop_agent/:

Tool-Based Approval

from strands import Agent
from strands.tools import tool
import json

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """
    Send an email after human approval.
    
    Args:
        to: Recipient email address
        subject: Email subject line
        body: Email body content
    """
    # Display proposed email
    print("\n" + "="*60)
    print("📧 EMAIL APPROVAL REQUEST")
    print("="*60)
    print(f"To: {to}")
    print(f"Subject: {subject}")
    print(f"\nBody:\n{body}")
    print("="*60)
    
    # Get approval
    approval = input("\nSend this email? (yes/no): ").lower().strip()
    
    if approval in ['yes', 'y']:
        # Actually send the email
        send_via_smtp(to, subject, body)
        return f"✅ Email sent to {to}"
    else:
        return "❌ Email cancelled by user"

# Agent with approval-gated tools
agent = Agent(
    model=model,
    system_prompt="You are a helpful email assistant",
    tools=[send_email]  # Tool requires human approval
)

Financial Transaction Approval

@tool
def execute_trade(
    symbol: str,
    action: str,  # "buy" or "sell"
    quantity: int,
    price: float
) -> str:
    """
    Execute a stock trade after human approval.
    
    Args:
        symbol: Stock ticker symbol
        action: "buy" or "sell"
        quantity: Number of shares
        price: Price per share
    """
    total_value = quantity * price
    
    # Display trade details
    print(f"\n💰 TRADE APPROVAL REQUEST")
    print(f"Action: {action.upper()} {quantity} shares of {symbol}")
    print(f"Price: ${price:.2f} per share")
    print(f"Total: ${total_value:,.2f}")
    
    # Get approval with additional verification
    confirm = input(f"\nType '{symbol}' to confirm: ").upper()
    
    if confirm == symbol:
        # Execute trade
        result = broker_api.execute_trade(
            symbol=symbol,
            action=action,
            quantity=quantity,
            price=price
        )
        return f"✅ Trade executed: {action} {quantity} {symbol} @ ${price}"
    else:
        return "❌ Trade cancelled"

Streamlit Interactive Approval

For web-based applications, use Streamlit for interactive approval workflows.

Example: Content Publishing

import streamlit as st
from agno.agent import Agent

def content_approval_workflow():
    st.title("Content Publishing Workflow")
    
    # Initialize session state
    if 'draft_content' not in st.session_state:
        st.session_state.draft_content = None
    if 'approved' not in st.session_state:
        st.session_state.approved = False
    
    # Step 1: Generate content
    topic = st.text_input("Enter topic for article")
    
    if st.button("Generate Draft") and topic:
        with st.spinner("Generating content..."):
            agent = Agent(
                model=model,
                instructions="Write a comprehensive article"
            )
            st.session_state.draft_content = agent.run(topic).content
    
    # Step 2: Review and approve
    if st.session_state.draft_content:
        st.subheader("📄 Draft Content")
        
        # Editable draft
        edited_content = st.text_area(
            "Review and edit content:",
            value=st.session_state.draft_content,
            height=400
        )
        
        col1, col2 = st.columns(2)
        
        with col1:
            if st.button("✅ Approve & Publish", type="primary"):
                # Publish content
                publish_to_cms(edited_content)
                st.success("✅ Content published successfully!")
                st.session_state.draft_content = None
        
        with col2:
            if st.button("❌ Reject"):
                st.session_state.draft_content = None
                st.warning("Draft rejected")
                st.rerun()

Multi-Stage Approval

import streamlit as st

def multi_stage_approval(workflow_steps: list):
    """
    Multi-stage approval workflow with progress tracking.
    
    Args:
        workflow_steps: List of (step_name, agent_function) tuples
    """
    if 'current_step' not in st.session_state:
        st.session_state.current_step = 0
    if 'results' not in st.session_state:
        st.session_state.results = {}
    
    # Progress bar
    progress = st.session_state.current_step / len(workflow_steps)
    st.progress(progress)
    
    # Current step
    if st.session_state.current_step < len(workflow_steps):
        step_name, agent_func = workflow_steps[st.session_state.current_step]
        
        st.subheader(f"Step {st.session_state.current_step + 1}: {step_name}")
        
        # Execute step
        if st.button(f"Run {step_name}"):
            with st.spinner(f"Executing {step_name}..."):
                result = agent_func()
                st.session_state.results[step_name] = result
        
        # Show result and approval
        if step_name in st.session_state.results:
            st.markdown("**Result:**")
            st.write(st.session_state.results[step_name])
            
            col1, col2 = st.columns(2)
            with col1:
                if st.button("✅ Approve & Continue"):
                    st.session_state.current_step += 1
                    st.rerun()
            
            with col2:
                if st.button("🔄 Regenerate"):
                    del st.session_state.results[step_name]
                    st.rerun()
    else:
        st.success("✅ All steps approved!")
        st.balloons()

Approval with Modifications

Allow humans to modify agent outputs before approval.

Editable Agent Output

from pydantic import BaseModel
import json

class EmailDraft(BaseModel):
    to: str
    subject: str
    body: str
    cc: list[str] = []
    bcc: list[str] = []

def email_with_edits(agent: Agent, request: str) -> EmailDraft:
    """
    Generate email draft and allow human editing.
    """
    # Generate draft
    draft = agent.run(request, output_schema=EmailDraft).content
    
    print("\n📧 Generated Email Draft")
    print(json.dumps(draft.model_dump(), indent=2))
    
    # Allow editing
    print("\nEdit fields (press Enter to keep current value):")
    
    to = input(f"To [{draft.to}]: ") or draft.to
    subject = input(f"Subject [{draft.subject}]: ") or draft.subject
    
    print(f"\nBody (current):\n{draft.body}")
    print("\nEnter new body (or press Enter twice to keep current):")
    lines = []
    while True:
        line = input()
        if line == "" and (not lines or lines[-1] == ""):
            break
        lines.append(line)
    
    body = "\n".join(lines).strip() or draft.body
    
    # Create edited draft
    edited_draft = EmailDraft(
        to=to,
        subject=subject,
        body=body,
        cc=draft.cc,
        bcc=draft.bcc
    )
    
    # Final confirmation
    print("\nFinal Email:")
    print(json.dumps(edited_draft.model_dump(), indent=2))
    
    if input("\nSend? (yes/no): ").lower() == 'yes':
        send_email(edited_draft)
        return edited_draft
    else:
        raise ValueError("Email cancelled by user")

Async Approval Workflows

For production systems, implement async approval queues.

Approval Queue Pattern

from enum import Enum
from datetime import datetime
from pydantic import BaseModel
import uuid

class ApprovalStatus(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    EXPIRED = "expired"

class ApprovalRequest(BaseModel):
    request_id: str
    action: str
    context: dict
    status: ApprovalStatus = ApprovalStatus.PENDING
    created_at: datetime
    expires_at: datetime
    approved_by: str | None = None

class ApprovalQueue:
    """Manage async approval requests."""
    
    def __init__(self):
        self.requests = {}  # In production, use a database
    
    def create_request(
        self,
        action: str,
        context: dict,
        expires_in_hours: int = 24
    ) -> str:
        """Create a new approval request."""
        request_id = str(uuid.uuid4())
        
        request = ApprovalRequest(
            request_id=request_id,
            action=action,
            context=context,
            created_at=datetime.now(),
            expires_at=datetime.now() + timedelta(hours=expires_in_hours)
        )
        
        self.requests[request_id] = request
        
        # Notify approvers (email, Slack, etc.)
        notify_approvers(request)
        
        return request_id
    
    def get_request(self, request_id: str) -> ApprovalRequest:
        """Get approval request by ID."""
        if request_id not in self.requests:
            raise ValueError(f"Request {request_id} not found")
        
        request = self.requests[request_id]
        
        # Check expiration
        if datetime.now() > request.expires_at:
            request.status = ApprovalStatus.EXPIRED
        
        return request
    
    def approve(self, request_id: str, approver: str) -> ApprovalRequest:
        """Approve a request."""
        request = self.get_request(request_id)
        
        if request.status != ApprovalStatus.PENDING:
            raise ValueError(f"Request is {request.status}, cannot approve")
        
        request.status = ApprovalStatus.APPROVED
        request.approved_by = approver
        
        return request
    
    def reject(self, request_id: str, approver: str) -> ApprovalRequest:
        """Reject a request."""
        request = self.get_request(request_id)
        
        if request.status != ApprovalStatus.PENDING:
            raise ValueError(f"Request is {request.status}, cannot reject")
        
        request.status = ApprovalStatus.REJECTED
        request.approved_by = approver
        
        return request

# Usage in agent workflow
queue = ApprovalQueue()

async def agent_with_async_approval(query: str):
    # Generate recommendation
    result = agent.run(query)
    
    # Create approval request
    request_id = queue.create_request(
        action="Execute agent recommendation",
        context={"query": query, "result": result.content}
    )
    
    print(f"Approval request created: {request_id}")
    print("Waiting for approval...")
    
    # Poll for approval (in production, use webhooks/events)
    while True:
        request = queue.get_request(request_id)
        
        if request.status == ApprovalStatus.APPROVED:
            return execute_action(result)
        elif request.status == ApprovalStatus.REJECTED:
            return "Action rejected by approver"
        elif request.status == ApprovalStatus.EXPIRED:
            return "Approval request expired"
        
        await asyncio.sleep(5)  # Check every 5 seconds

Best Practices

1. Clear Approval Context

# ✅ Good: Provide full context
def request_approval(action: str, details: dict):
    print(f"Action: {action}")
    print(f"Details: {json.dumps(details, indent=2)}")
    print(f"Risks: {assess_risks(details)}")
    print(f"Impact: {estimate_impact(details)}")
    return get_user_input()

# ❌ Bad: Vague approval request
def request_approval(action: str):
    return input(f"Approve {action}? ")  # No context

2. Set Reasonable Timeouts

# ✅ Good: Expire old requests
request = ApprovalRequest(
    ...,
    expires_at=datetime.now() + timedelta(hours=24)
)

# ❌ Bad: No expiration (pending forever)
request = ApprovalRequest(...)  # No timeout

3. Allow Modifications

# ✅ Good: Allow editing before approval
edited = allow_user_edits(agent_output)
if approve(edited):
    execute(edited)

# ❌ Bad: Binary approve/reject only
if approve(agent_output):  # Can't modify
    execute(agent_output)

4. Audit Trail

# ✅ Good: Log all approval decisions
class ApprovalLog:
    request_id: str
    action: str
    status: ApprovalStatus
    approver: str
    timestamp: datetime
    reason: str | None

def log_approval(request_id, status, approver, reason=None):
    log = ApprovalLog(
        request_id=request_id,
        status=status,
        approver=approver,
        timestamp=datetime.now(),
        reason=reason
    )
    save_to_database(log)

# ❌ Bad: No logging
def approve(request):
    request.status = "approved"  # No audit trail

Real-World Examples

Email Agent with Approval

Location: course/aws_strands/05_human_in_the_loop_agent/ Human approves every email before sending.

Finance Agent

Location: advance_ai_agents/agentfield_finance_research_agent/ Investment recommendations require approval before execution.

Content Publishing

Location: advance_ai_agents/content_team_agent/ SEO-optimized content reviewed before publishing.

Next Steps

Multi-Agent Patterns

Coordinate multiple agents with approval gates

Memory Systems

Remember approval history and user preferences

Best Practices

Production patterns for approval workflows

Environment Setup

Configure development environment for HITL testing

Build docs developers (and LLMs) love