showcase/shell-docs/src/content/reference/v1/hooks/useHumanInTheLoop.mdx
useHumanInTheLoop pauses AI execution to request human input or approval. When the AI calls this
tool, it stops and waits for the user to respond through your custom UI before continuing. This is
essential for sensitive operations, confirmations, or collecting information that only the user can provide.
Unlike useFrontendTool, there's no handler function—instead, your render function receives a respond
callback that sends the user's input back to the AI. The AI execution remains paused until respond is called,
making this a true blocking interaction.
import { useHumanInTheLoop } from "@copilotkit/react-core";
useHumanInTheLoop({
name: "confirmDeletion",
description: "Ask user to confirm before deleting items",
parameters: [
{
name: "itemName",
type: "string",
description: "Name of the item to delete",
required: true,
},
{
name: "itemCount",
type: "number",
description: "Number of items to delete",
required: true,
},
],
render: ({ args, status, respond, result }) => {
if (status === "executing" && respond) {
return (
<div className="p-4 border rounded">
<p>Are you sure you want to delete {args.itemCount} {args.itemName}(s)?</p>
<div className="flex gap-2 mt-4">
<button
onClick={() => respond({ confirmed: true })}
className="bg-red-500 text-white px-4 py-2 rounded"
>
Delete
</button>
<button
onClick={() => respond({ confirmed: false })}
className="bg-gray-300 px-4 py-2 rounded"
>
Cancel
</button>
</div>
</div>
);
}
if (status === "complete" && result) {
return (
<div className="p-2 text-sm text-gray-600">
{result.confirmed ? "Items deleted" : "Deletion cancelled"}
</div>
);
}
return null;
},
});
import { useHumanInTheLoop } from "@copilotkit/react-core";
import { useState } from "react";
useHumanInTheLoop({
name: "collectUserPreferences",
description: "Collect detailed preferences from the user",
parameters: [
{
name: "context",
type: "string",
description: "Context for why preferences are needed",
required: true,
},
{
name: "requiredFields",
type: "string[]",
description: "Fields to collect",
required: true,
},
],
render: ({ args, status, respond }) => {
const [preferences, setPreferences] = useState({
theme: "light",
notifications: true,
language: "en",
});
if (status === "executing" && respond) {
return (
<div className="p-4 border rounded">
<h3 className="font-bold mb-2">{args.context}</h3>
<form onSubmit={(e) => {
e.preventDefault();
respond(preferences);
}}>
<button
type="submit"
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Save Preferences
</button>
</form>
</div>
);
}
return null;
},
});
respond function before rendering interactive elementsrespondIf you're migrating from useCopilotAction with renderAndWaitForResponse:
// Before with useCopilotAction
useCopilotAction({
name: "confirmAction",
parameters: [
{ name: "message", type: "string", required: true },
],
renderAndWaitForResponse: ({ args, respond, status }) => {
return (
<ConfirmDialog
message={args.message}
onConfirm={() => respond(true)}
onCancel={() => respond(false)}
isActive={status === "executing"}
/>
);
},
});
// After with useHumanInTheLoop
useHumanInTheLoop({
name: "confirmAction",
parameters: [
{
name: "message",
type: "string",
description: "The message to display",
required: true,
},
],
render: ({ args, respond, status }) => {
if (status === "executing" && respond) {
return (
<ConfirmDialog
message={args.message}
onConfirm={() => respond(true)}
onCancel={() => respond(false)}
isActive={true}
/>
);
}
return null;
},
});
The main differences are:
render instead of renderAndWaitForResponserespond function's existenceSimple example: [{ name: "itemName", type: "string", description: "Name of the item", required: true }]
Nested example:
[
{
name: "approval",
type: "object",
description: "Approval request details",
required: true,
properties: [
{ name: "action", type: "string", description: "Action requiring approval", required: true },
{ name: "reason", type: "string", description: "Reason for the action", required: false }
]
}
]