Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CopilotKit/CopilotKit/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The useHumanInTheLoop hook creates interactive tools that pause agent execution to request user input, approval, or interaction before continuing. This enables human oversight and collaboration in AI workflows.

Usage

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";

function ApprovalFlow() {
  useHumanInTheLoop({
    name: "requestApproval",
    description: "Request user approval for an action",
    parameters: z.object({
      action: z.string(),
      details: z.string(),
    }),
    render: ({ args, status, respond }) => {
      if (status === "executing") {
        return (
          <div>
            <p>Approve: {args.action}</p>
            <p>{args.details}</p>
            <button onClick={() => respond({ approved: true })}>
              Approve
            </button>
            <button onClick={() => respond({ approved: false })}>
              Reject
            </button>
          </div>
        );
      }
      return <div>Waiting for approval...</div>;
    },
  });
  
  return <div>...</div>;
}

Type Signature

function useHumanInTheLoop<T extends Record<string, unknown>>(
  tool: ReactHumanInTheLoop<T>,
  deps?: ReadonlyArray<unknown>
): void

Parameters

tool
ReactHumanInTheLoop<T>
required
The human-in-the-loop tool definition with the following properties:
deps
ReadonlyArray<unknown>
Optional dependency array. When dependencies change, the tool is re-registered.
useHumanInTheLoop(tool, [userId, permissions]);

Return Value

This hook does not return a value. The interactive tool is registered and automatically unregistered on unmount.

Examples

Simple Approval

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";

function ApprovalTool() {
  useHumanInTheLoop({
    name: "requestApproval",
    description: "Get user approval for an action",
    parameters: z.object({
      action: z.string(),
    }),
    render: ({ args, status, respond }) => {
      if (status === "executing") {
        return (
          <div className="approval-dialog">
            <h3>Approval Required</h3>
            <p>Action: {args.action}</p>
            <div className="buttons">
              <button 
                onClick={() => respond({ approved: true })}
                className="approve"
              >
                Approve
              </button>
              <button 
                onClick={() => respond({ approved: false })}
                className="reject"
              >
                Reject
              </button>
            </div>
          </div>
        );
      }
      
      if (status === "complete") {
        const result = JSON.parse(result);
        return (
          <div>
{result.approved ? "Approved" : "Rejected"}
          </div>
        );
      }
      
      return <div>Waiting for parameters...</div>;
    },
  });
  
  return null;
}

Form Input Request

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";
import { useState } from "react";

function UserInputTool() {
  useHumanInTheLoop({
    name: "getUserInput",
    description: "Request specific information from the user",
    parameters: z.object({
      prompt: z.string(),
      fieldName: z.string(),
    }),
    render: ({ args, status, respond }) => {
      const [input, setInput] = useState("");
      
      if (status === "executing") {
        return (
          <div className="input-dialog">
            <label>{args.prompt}</label>
            <input
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder={args.fieldName}
            />
            <button 
              onClick={() => respond({ [args.fieldName]: input })}
              disabled={!input}
            >
              Submit
            </button>
          </div>
        );
      }
      
      if (status === "complete") {
        return <div>✓ Input received</div>;
      }
      
      return <div>Loading...</div>;
    },
  });
  
  return null;
}

Multi-Step Confirmation

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";

function DeleteConfirmation() {
  useHumanInTheLoop({
    name: "confirmDelete",
    description: "Confirm deletion of important data",
    parameters: z.object({
      itemType: z.string(),
      itemName: z.string(),
      itemId: z.string(),
      consequences: z.array(z.string()),
    }),
    render: ({ args, status, respond }) => {
      if (status === "executing") {
        return (
          <div className="delete-confirmation">
            <h3>⚠️ Confirm Deletion</h3>
            <p>
              You are about to delete {args.itemType}: <strong>{args.itemName}</strong>
            </p>
            
            <div className="consequences">
              <p>This will:</p>
              <ul>
                {args.consequences.map((consequence, idx) => (
                  <li key={idx}>{consequence}</li>
                ))}
              </ul>
            </div>
            
            <p className="warning">
              This action cannot be undone.
            </p>
            
            <div className="buttons">
              <button 
                onClick={() => respond({ confirmed: false })}
                className="secondary"
              >
                Cancel
              </button>
              <button 
                onClick={() => respond({ 
                  confirmed: true, 
                  itemId: args.itemId 
                })}
                className="danger"
              >
                Delete Permanently
              </button>
            </div>
          </div>
        );
      }
      
      if (status === "complete") {
        const result = JSON.parse(result);
        return result.confirmed ? (
          <div className="success">✓ Item deleted</div>
        ) : (
          <div className="info">Deletion cancelled</div>
        );
      }
      
      return <div>Preparing...</div>;
    },
  });
  
  return null;
}

Selection Dialog

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";

function OptionSelector() {
  useHumanInTheLoop({
    name: "selectOption",
    description: "Present options for user to choose from",
    parameters: z.object({
      question: z.string(),
      options: z.array(z.object({
        id: z.string(),
        label: z.string(),
        description: z.string().optional(),
      })),
    }),
    render: ({ args, status, respond }) => {
      if (status === "executing") {
        return (
          <div className="option-selector">
            <h3>{args.question}</h3>
            <div className="options">
              {args.options.map((option) => (
                <button
                  key={option.id}
                  className="option-card"
                  onClick={() => respond({ 
                    selectedId: option.id,
                    selectedLabel: option.label 
                  })}
                >
                  <div className="label">{option.label}</div>
                  {option.description && (
                    <div className="description">{option.description}</div>
                  )}
                </button>
              ))}
            </div>
          </div>
        );
      }
      
      if (status === "complete") {
        const result = JSON.parse(result);
        return <div>Selected: {result.selectedLabel}</div>;
      }
      
      return <div>Loading options...</div>;
    },
  });
  
  return null;
}

File Upload Request

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";
import { useState } from "react";

function FileUploadTool() {
  useHumanInTheLoop({
    name: "requestFile",
    description: "Request a file upload from the user",
    parameters: z.object({
      fileType: z.string(),
      purpose: z.string(),
    }),
    render: ({ args, status, respond }) => {
      const [uploading, setUploading] = useState(false);
      
      const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
        const file = e.target.files?.[0];
        if (!file) return;
        
        setUploading(true);
        try {
          // Upload file
          const formData = new FormData();
          formData.append("file", file);
          const response = await fetch("/api/upload", {
            method: "POST",
            body: formData,
          });
          const data = await response.json();
          
          // Respond with file info
          await respond({
            fileName: file.name,
            fileSize: file.size,
            fileUrl: data.url,
          });
        } catch (error) {
          console.error("Upload failed:", error);
        } finally {
          setUploading(false);
        }
      };
      
      if (status === "executing") {
        return (
          <div className="file-upload">
            <h3>File Upload Required</h3>
            <p>Purpose: {args.purpose}</p>
            <p>Expected type: {args.fileType}</p>
            <input
              type="file"
              onChange={handleFileSelect}
              accept={args.fileType}
              disabled={uploading}
            />
            {uploading && <div>Uploading...</div>}
          </div>
        );
      }
      
      if (status === "complete") {
        const result = JSON.parse(result);
        return <div>✓ Uploaded: {result.fileName}</div>;
      }
      
      return <div>Preparing upload...</div>;
    },
  });
  
  return null;
}

Conditional Availability

import { useHumanInTheLoop } from "@copilotkit/react";
import { z } from "zod";

function AdminApproval({ user }) {
  useHumanInTheLoop({
    name: "adminApproval",
    description: "Request admin approval",
    parameters: z.object({
      action: z.string(),
    }),
    render: ({ args, status, respond }) => {
      if (status === "executing") {
        return (
          <div>
            <p>Admin approval needed: {args.action}</p>
            <button onClick={() => respond({ approved: true })}>
              Approve (Admin)
            </button>
          </div>
        );
      }
      return <div>Complete</div>;
    },
    available: user.role === "admin", // Only available to admins
  }, [user.role]);
  
  return null;
}

Behavior

Execution Flow

  1. inProgress: Agent is streaming the tool call parameters
  2. executing: Parameters complete, waiting for user interaction via respond()
  3. complete: User responded, agent continues with the result

Respond Function

The respond() function:
  • Accepts any value (typically an object)
  • Returns a Promise that resolves when the response is sent
  • Can only be called once per tool execution
  • Resumes agent execution with the provided result

Cleanup

  • The render component is removed from the registry on unmount
  • This prevents ghost interactions after the component is gone
  • Different from useFrontendTool, which keeps renderers for history

Use Cases

Approvals

Request user approval before executing sensitive operations like deletions, payments, or data modifications.

Data Input

Collect specific information from users that the agent needs but doesn’t have access to.

Confirmations

Confirm important decisions or actions with the user before proceeding.

Selections

Present options and let users make choices that guide the agent’s next steps.

File Uploads

Request files from users that the agent needs to process or analyze.

Notes

The render component must handle all three status states (inProgress, executing, complete) to provide a complete user experience.
Human-in-the-loop tools automatically pause agent execution. The agent will not continue until respond() is called.
If your component unmounts while a human-in-the-loop tool is in the executing state, the interaction will be lost. Consider hoisting these components to a stable parent.

Build docs developers (and LLMs) love