Skip to main content
The useHumanInTheLoop hook creates tools that pause agent execution and wait for human interaction. This is perfect for approval workflows, confirmation dialogs, or any scenario where you need user input before the agent continues.

Basic Usage

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

function ApprovalTool() {
  useHumanInTheLoop({
    name: "requestApproval",
    description: "Request approval for an action",
    parameters: z.object({
      action: z.string(),
      reason: z.string()
    }),
    render: ({ status, args, respond }) => {
      if (status === "executing" && respond) {
        return (
          <div>
            <p>Approve {args.action}?</p>
            <p>Reason: {args.reason}</p>
            <button onClick={() => respond({ approved: true })}>
              Approve
            </button>
            <button onClick={() => respond({ approved: false })}>
              Reject
            </button>
          </div>
        );
      }
      
      if (status === "complete") {
        return <div>Decision recorded</div>;
      }
      
      return <div>Preparing request...</div>;
    }
  });
  
  return null;
}

Parameters

The hook accepts a tool object with the following properties:
name
string
required
Unique identifier for the tool.
description
string
Human-readable description of what the tool does.
parameters
z.ZodType<T>
Zod schema defining the tool’s parameters.
render
React.ComponentType
required
React component that renders the human-in-the-loop interface. Receives different props based on execution status.
agentId
string
Scope this tool to a specific agent.
available
boolean
Whether the tool is currently available. Defaults to true.

Dependencies

deps
ReadonlyArray<unknown>
Optional dependency array for re-registering the tool when dependencies change.

Render Props

Your render component receives different props based on the tool’s status:

inProgress Status

{
  name: string;
  description: string;
  args: Partial<T>;
  status: "inProgress";
  result: undefined;
  respond: undefined;
}
Tool call is streaming in, parameters may be incomplete.

executing Status

{
  name: string;
  description: string;
  args: T;
  status: "executing";
  result: undefined;
  respond: (result: unknown) => Promise<void>;
}
Parameters are complete, waiting for user interaction. Use respond() to provide the result.

complete Status

{
  name: string;
  description: string;
  args: T;
  status: "complete";
  result: string;
  respond: undefined;
}
User has responded, tool execution is complete.

Examples

Approval Workflow

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

function ApprovalWorkflow() {
  useHumanInTheLoop({
    name: "approveDelete",
    description: "Request approval to delete an item",
    parameters: z.object({
      itemId: z.string(),
      itemName: z.string(),
      reason: z.string()
    }),
    render: ({ status, args, respond, result }) => {
      if (status === "inProgress") {
        return <div>Loading approval request...</div>;
      }
      
      if (status === "executing" && respond) {
        return (
          <div className="approval-card">
            <h3>Deletion Approval Required</h3>
            <p><strong>Item:</strong> {args.itemName}</p>
            <p><strong>Reason:</strong> {args.reason}</p>
            <div className="actions">
              <button 
                className="approve"
                onClick={() => respond({ 
                  approved: true, 
                  timestamp: Date.now() 
                })}
              >
                Approve Deletion
              </button>
              <button 
                className="reject"
                onClick={() => respond({ 
                  approved: false,
                  reason: "User rejected"
                })}
              >
                Cancel
              </button>
            </div>
          </div>
        );
      }
      
      if (status === "complete") {
        const decision = JSON.parse(result);
        return (
          <div className={decision.approved ? "approved" : "rejected"}>
            {decision.approved ? "✓ Approved" : "✗ Rejected"}
          </div>
        );
      }
    }
  });
  
  return null;
}

Data Input Form

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

function DataInputTool() {
  useHumanInTheLoop({
    name: "collectUserData",
    description: "Collect additional information from the user",
    parameters: z.object({
      fields: z.array(z.object({
        name: z.string(),
        label: z.string(),
        type: z.enum(["text", "number", "email"])
      }))
    }),
    render: ({ status, args, respond }) => {
      const [formData, setFormData] = useState<Record<string, string>>({});
      
      if (status === "executing" && respond) {
        return (
          <div className="data-form">
            <h3>Please Provide Information</h3>
            {args.fields.map(field => (
              <div key={field.name}>
                <label>{field.label}</label>
                <input
                  type={field.type}
                  value={formData[field.name] || ""}
                  onChange={(e) => setFormData({
                    ...formData,
                    [field.name]: e.target.value
                  })}
                />
              </div>
            ))}
            <button onClick={() => respond(formData)}>
              Submit
            </button>
          </div>
        );
      }
      
      if (status === "complete") {
        return <div>Information submitted successfully</div>;
      }
      
      return <div>Preparing form...</div>;
    }
  });
  
  return null;
}

Multiple Choice Selection

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

function SelectionTool() {
  useHumanInTheLoop({
    name: "selectOption",
    description: "Ask user to select from multiple options",
    parameters: z.object({
      question: z.string(),
      options: z.array(z.string())
    }),
    render: ({ status, args, respond, result }) => {
      if (status === "executing" && respond) {
        return (
          <div className="selection-tool">
            <h3>{args.question}</h3>
            <div className="options">
              {args.options.map((option, index) => (
                <button
                  key={index}
                  onClick={() => respond({ 
                    selectedIndex: index,
                    selectedValue: option 
                  })}
                  className="option-button"
                >
                  {option}
                </button>
              ))}
            </div>
          </div>
        );
      }
      
      if (status === "complete") {
        const selection = JSON.parse(result);
        return (
          <div className="selection-complete">
            Selected: {selection.selectedValue}
          </div>
        );
      }
      
      return <div>Loading options...</div>;
    }
  });
  
  return null;
}

File Upload Tool

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

function FileUploadTool() {
  useHumanInTheLoop({
    name: "uploadFile",
    description: "Request user to upload a file",
    parameters: z.object({
      purpose: z.string(),
      acceptedTypes: z.array(z.string())
    }),
    render: ({ status, args, respond }) => {
      const [uploading, setUploading] = useState(false);
      
      const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
        const file = e.target.files?.[0];
        if (!file || !respond) return;
        
        setUploading(true);
        
        // 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({
          fileName: file.name,
          fileSize: file.size,
          uploadedUrl: data.url
        });
        
        setUploading(false);
      };
      
      if (status === "executing" && respond) {
        return (
          <div className="upload-tool">
            <h3>Upload File</h3>
            <p>Purpose: {args.purpose}</p>
            <input
              type="file"
              accept={args.acceptedTypes.join(",")}
              onChange={handleFileUpload}
              disabled={uploading}
            />
            {uploading && <p>Uploading...</p>}
          </div>
        );
      }
      
      if (status === "complete") {
        return <div>File uploaded successfully</div>;
      }
      
      return <div>Preparing upload...</div>;
    }
  });
  
  return null;
}

Confirmation Dialog

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

function ConfirmationTool() {
  useHumanInTheLoop({
    name: "confirmAction",
    description: "Ask for user confirmation",
    parameters: z.object({
      title: z.string(),
      message: z.string(),
      severity: z.enum(["info", "warning", "danger"])
    }),
    render: ({ status, args, respond }) => {
      if (status === "executing" && respond) {
        return (
          <div className={`confirmation ${args.severity}`}>
            <h3>{args.title}</h3>
            <p>{args.message}</p>
            <div className="buttons">
              <button 
                className="confirm"
                onClick={() => respond({ confirmed: true })}
              >
                Confirm
              </button>
              <button 
                className="cancel"
                onClick={() => respond({ confirmed: false })}
              >
                Cancel
              </button>
            </div>
          </div>
        );
      }
      
      if (status === "complete") {
        const result = JSON.parse(result);
        return (
          <div>
            {result.confirmed ? "Confirmed" : "Cancelled"}
          </div>
        );
      }
      
      return null;
    }
  });
  
  return null;
}

How It Works

  1. Agent calls the tool - The agent decides to use your human-in-the-loop tool
  2. Parameters stream in - Status is “inProgress” while parameters are received
  3. Execution pauses - Status becomes “executing”, your UI is rendered with respond callback
  4. User interacts - User provides input and calls respond() with the result
  5. Tool completes - Status becomes “complete”, agent receives the result and continues

Best Practices

Clear Instructions

Provide clear instructions in your UI about what the user needs to do. Include context about why the interaction is needed.

Handle All States

Always handle all three states (inProgress, executing, complete) in your render function for a smooth user experience.

Validate Input

Validate user input before calling respond(). Provide error messages if the input is invalid.

Timeout Handling

Consider implementing a timeout for user interactions. If the user doesn’t respond, you may want to cancel or provide a default response.

Differences from useFrontendTool

FeatureuseHumanInTheLoopuseFrontendTool
HandlerAutomatic (waits for respond())Custom async function
User InteractionRequiredOptional (via render)
ExecutionPauses until user respondsRuns immediately
Render PropsIncludes respond callbackNo respond callback
Use CaseApprovals, confirmations, inputData fetching, calculations

Cleanup Behavior

Unlike useFrontendTool, when a component with useHumanInTheLoop unmounts:
  • Both the handler and renderer are removed
  • Historical tool calls will not be able to respond to new interactions
  • This prevents orphaned interactive elements after component unmount

TypeScript

interface ReactHumanInTheLoop<T extends Record<string, unknown> = Record<string, unknown>> {
  name: string;
  description?: string;
  parameters?: z.ZodType<T>;
  agentId?: string;
  available?: boolean;
  render: React.ComponentType<
    | {
        name: string;
        description: string;
        args: Partial<T>;
        status: "inProgress";
        result: undefined;
        respond: undefined;
      }
    | {
        name: string;
        description: string;
        args: T;
        status: "executing";
        result: undefined;
        respond: (result: unknown) => Promise<void>;
      }
    | {
        name: string;
        description: string;
        args: T;
        status: "complete";
        result: string;
        respond: undefined;
      }
  >;
}