BotsHuman Handoffs

Human Handoffs

AI does the work. Humans approve the decisions.

The human handoff system lets a workflow pause execution, notify a human reviewer with full context, and wait for approval or rejection before continuing. The reviewer sees exactly what happened in the conversation, why the handoff was triggered, and has one-click approve/reject controls.


How It Works

  1. The workflow reaches a human_handoff action node
  2. Execution pauses — the workflow state is persisted
  3. A notification is sent to the configured reviewers with:
    • The escalation reason (from the node’s reason field)
    • The full conversation history
    • Relevant variables (order details, amounts, customer info)
    • Approve and Reject buttons
  4. The reviewer reviews the context and makes a decision
  5. The workflow resumes on the approved or rejected edge

The user receives a message like: “I have escalated this to a reviewer. You will hear back shortly.”


Setting Up the human_handoff Node

Add a human_handoff action to your workflow:

{
  "type": "action",
  "data": {
    "actionType": "human_handoff",
    "reason": "Refund of ${{refund_amount}} for order #{{order_id}} (customer: {{verified_email}})",
    "label": "Finance Approval"
  }
}

The reason field supports variable interpolation. Include the key details the reviewer needs to make a decision without leaving the notification interface.

Connecting Approved and Rejected Paths

Wire edges from the handoff node to downstream nodes:

{ "source": "handoff_1", "target": "process_action", "sourceHandle": "approved" },
{ "source": "handoff_1", "target": "send_rejection", "sourceHandle": "rejected" }

The approved path executes when the reviewer approves. The rejected path executes when the reviewer rejects.


Notification Channels

Reviewers can be notified through multiple channels. Configure notification preferences in your bot’s settings.

ChannelHow It Works
Web DashboardNotification appears in the Admin Dashboard under the bot’s handoff queue. Reviewer clicks Approve or Reject directly.
EmailReviewer receives an email with the conversation summary, reason, and action links.
TelegramNotification sent to a Telegram user or group with inline Approve/Reject buttons.
DiscordNotification sent to a Discord channel with button components.

What the Reviewer Sees

The notification includes:

FieldContent
Bot nameWhich bot triggered the handoff
ReasonThe interpolated reason string from the node
Conversation summaryRecent messages between the user and the bot
Key variablesOrder details, amounts, customer info — whatever variables are set
TimestampWhen the handoff was triggered
ActionsApprove / Reject buttons

The reviewer does not need to log in to the full dashboard to take action. Email and messaging notifications include direct action links.


Use Cases

High-Value Refund Approval

A customer asks for a $2,000 refund. The bot:

  1. Verifies identity via auth_gate
  2. Looks up the order and confirms eligibility
  3. Detects the amount exceeds $500
  4. Triggers human_handoff with reason: “Refund of $2,000 for order #45678”
  5. The finance manager receives a Telegram notification
  6. Manager reviews the order details and approves
  7. The workflow resumes and processes the refund via Stripe MCP

Contract Review

A sales bot generates a proposal for a prospect. Before sending:

  1. The workflow assembles the proposal details
  2. human_handoff notifies the legal team: “New proposal for Enterprise plan, $50k/year”
  3. Legal reviews terms and approves
  4. The bot sends the approved proposal to the prospect

Access Requests

An IT helpdesk bot receives a request for production database access:

  1. Bot verifies the requester’s identity
  2. Checks their role and team via LDAP MCP
  3. human_handoff notifies the security team with the requester’s name and team
  4. Security approves or rejects with a reason

Compliance Review

A content moderation bot flags a post:

  1. AI analyzes the content and detects potential policy violation
  2. human_handoff sends the content to the moderation team
  3. Moderator reviews and decides: approve (content stays) or reject (content removed)

Escalation from Support

Any support interaction the bot cannot resolve:

  1. Bot attempts to help via agent loop or workflow
  2. After failing to resolve, triggers human_handoff
  3. Support agent picks up the conversation with full context

Best Practices

Write Descriptive Reasons

The reason string is the most important part of the notification. Include:

  • What is being requested
  • Who is requesting it
  • How much is at stake (dollar amounts, access levels, etc.)

Good: "Refund of $2,150 for order #45678 — customer verified as [email protected], order placed 3 days ago"

Bad: "Customer wants a refund"

Set Timeout Expectations

The user waits while the handoff is pending. Communicate clearly:

  • Tell the user their request has been escalated
  • Provide an estimated response time if possible
  • Offer alternative contact methods for urgent issues

Keep the Approved/Rejected Paths Complete

Both paths should send a message to the user:

  • Approved: “Your refund has been approved and processed.”
  • Rejected: “Your request was reviewed but could not be approved at this time. Please contact [email protected] for more details.”

Never leave the user without a response on either path.

Use auth_gate Before human_handoff

For any workflow that handles sensitive actions, verify the user’s identity before escalating. This ensures the reviewer sees a verified identity, not just a platform username.


Example: Complete Refund Workflow with Handoff

{
  "nodes": [
    { "id": "t1", "type": "trigger", "data": { "triggerType": "message_received" } },
    { "id": "auth", "type": "auth_gate", "data": { "provider": "resend", "contact_type": "email", "outputVariable": "email" } },
    { "id": "lookup", "type": "mcp_tool", "data": { "toolName": "orders__get_order", "arguments": "{\"email\": \"{{email}}\"}", "outputVariable": "order" } },
    { "id": "check", "type": "logic", "data": { "logicType": "if_else", "condition": "{{order.total}} > 500" } },
    { "id": "handoff", "type": "action", "data": { "actionType": "human_handoff", "reason": "Refund ${{order.total}} for order #{{order.id}} ({{email}})" } },
    { "id": "auto_refund", "type": "mcp_tool", "data": { "toolName": "stripe__create_refund", "arguments": "{\"order_id\": \"{{order.id}}\"}", "outputVariable": "refund" } },
    { "id": "approved_refund", "type": "mcp_tool", "data": { "toolName": "stripe__create_refund", "arguments": "{\"order_id\": \"{{order.id}}\"}", "outputVariable": "refund" } },
    { "id": "msg_approved", "type": "send_message", "data": { "message": "Your refund of ${{order.total}} has been approved and processed." } },
    { "id": "msg_rejected", "type": "send_message", "data": { "message": "Your refund request was reviewed but could not be approved. Please contact [email protected]." } },
    { "id": "msg_auto", "type": "send_message", "data": { "message": "Your refund of ${{order.total}} for order #{{order.id}} has been processed." } }
  ],
  "edges": [
    { "id": "e1", "source": "t1", "target": "auth" },
    { "id": "e2", "source": "auth", "target": "lookup", "sourceHandle": "verified" },
    { "id": "e3", "source": "lookup", "target": "check" },
    { "id": "e4", "source": "check", "target": "handoff", "sourceHandle": "true" },
    { "id": "e5", "source": "check", "target": "auto_refund", "sourceHandle": "false" },
    { "id": "e6", "source": "handoff", "target": "approved_refund", "sourceHandle": "approved" },
    { "id": "e7", "source": "handoff", "target": "msg_rejected", "sourceHandle": "rejected" },
    { "id": "e8", "source": "approved_refund", "target": "msg_approved" },
    { "id": "e9", "source": "auto_refund", "target": "msg_auto" }
  ]
}

Next Steps