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:
Unique identifier for the tool.
Human-readable description of what the tool does.
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.
Scope this tool to a specific agent.
Whether the tool is currently available. Defaults to true.
Dependencies
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 ;
}
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 ;
}
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
Agent calls the tool - The agent decides to use your human-in-the-loop tool
Parameters stream in - Status is “inProgress” while parameters are received
Execution pauses - Status becomes “executing”, your UI is rendered with respond callback
User interacts - User provides input and calls respond() with the result
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.
Feature useHumanInTheLoop useFrontendTool Handler Automatic (waits for respond()) Custom async function User Interaction Required Optional (via render) Execution Pauses until user responds Runs immediately Render Props Includes respond callback No respond callback Use Case Approvals, confirmations, input Data 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 ;
}
>;
}