showcase/shell-docs/src/content/reference/hooks/useHumanInTheLoop.mdx
useHumanInTheLoop registers an interactive tool that pauses agent execution until the user responds through your custom UI. Unlike useFrontendTool, there is no handler function. Instead, the hook provides an internal status machine (InProgress -> Executing -> Complete) and supplies a respond callback to the render component while the tool is in the Executing state. The agent remains paused until respond is called with the user's input.
This hook is built on top of useFrontendTool with an internally managed handler that resolves the tool call promise when respond is invoked. Use it for confirmation dialogs, approval workflows, form collection, or any scenario where a human must provide input before the agent can continue.
import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
function useHumanInTheLoop<T extends Record<string, unknown>>(
tool: ReactHumanInTheLoop<T>,
deps?: ReadonlyArray<unknown>,
): void;
**When `status` is `ToolCallStatus.InProgress`:**
- `args: Partial<T>` -- partially streamed arguments
- `respond: undefined` -- not yet available
- `result: undefined`
**When `status` is `ToolCallStatus.Executing`:**
- `args: T` -- fully resolved arguments
- `respond: (result: unknown) => Promise<void>` -- call this to send the user's response back to the agent and resume execution
- `result: undefined`
**When `status` is `ToolCallStatus.Complete`:**
- `args: T` -- the original arguments
- `respond: undefined` -- no longer available
- `result: string` -- the serialized result
A common pattern where the agent asks the user to confirm a destructive action.
function DeleteConfirmation() {
useHumanInTheLoop(
{
name: "confirmDeletion",
description: "Ask the user to confirm before deleting items",
parameters: z.object({
itemName: z.string().describe("Name of the item to delete"),
itemCount: z.number().describe("Number of items to delete"),
}),
render: ({ args, status, respond, result }) => {
if (status === ToolCallStatus.InProgress) {
return (
<div className="p-4 text-gray-500">Preparing confirmation...</div>
);
}
if (status === ToolCallStatus.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 === ToolCallStatus.Complete && result) {
const parsed = JSON.parse(result);
return (
<div className="p-2 text-sm text-gray-600">
{parsed.confirmed ? "Items deleted." : "Deletion cancelled."}
</div>
);
}
return null;
},
},
[],
);
return null;
}
Collect structured input from the user before the agent proceeds.
function ShippingAddressForm() {
useHumanInTheLoop(
{
name: "collectShippingAddress",
description:
"Collect shipping address from the user before placing an order",
parameters: z.object({
orderSummary: z
.string()
.describe("A summary of the order being placed"),
}),
render: ({ args, status, respond }) => {
const [address, setAddress] = useState({
street: "",
city: "",
zip: "",
});
if (status === ToolCallStatus.Executing && respond) {
return (
<div className="p-4 border rounded space-y-3">
<p className="font-medium">Order: {args.orderSummary}</p>
<p>Please enter your shipping address:</p>
<input
placeholder="Street address"
value={address.street}
onChange={(e) =>
setAddress({ ...address, street: e.target.value })
}
className="w-full border p-2 rounded"
/>
<input
placeholder="City"
value={address.city}
onChange={(e) =>
setAddress({ ...address, city: e.target.value })
}
className="w-full border p-2 rounded"
/>
<input
placeholder="ZIP code"
value={address.zip}
onChange={(e) =>
setAddress({ ...address, zip: e.target.value })
}
className="w-full border p-2 rounded"
/>
<button
onClick={() => respond(address)}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
Submit Address
</button>
</div>
);
}
if (status === ToolCallStatus.Complete) {
return (
<div className="p-2 text-green-600">
Shipping address submitted.
</div>
);
}
return null;
},
},
[],
);
return null;
}
function ExpenseApproval() {
useHumanInTheLoop(
{
name: "approveExpense",
description: "Request manager approval for an expense report",
parameters: z.object({
employeeName: z.string().describe("Name of the employee"),
amount: z.number().describe("Expense amount in dollars"),
category: z.string().describe("Expense category"),
description: z.string().describe("Description of the expense"),
}),
render: ({ args, status, respond, result }) => {
if (status === ToolCallStatus.Executing && respond) {
return (
<div className="p-4 border rounded">
<h3 className="font-bold">Expense Approval Required</h3>
<div className="mt-2 space-y-1 text-sm">
<p>Employee: {args.employeeName}</p>
<p>Amount: ${args.amount.toFixed(2)}</p>
<p>Category: {args.category}</p>
<p>Description: {args.description}</p>
</div>
<div className="flex gap-2 mt-4">
<button
onClick={() => respond({ approved: true })}
className="bg-green-500 text-white px-4 py-2 rounded"
>
Approve
</button>
<button
onClick={() =>
respond({ approved: false, reason: "Needs more detail" })
}
className="bg-red-500 text-white px-4 py-2 rounded"
>
Reject
</button>
</div>
</div>
);
}
if (status === ToolCallStatus.Complete && result) {
const parsed = JSON.parse(result);
return (
<div
className={`p-2 text-sm ${parsed.approved ? "text-green-600" : "text-red-600"}`}
>
{parsed.approved
? "Expense approved."
: `Expense rejected: ${parsed.reason}`}
</div>
);
}
return null;
},
},
[],
);
return null;
}
respond callback to be invoked before continuing.InProgress (arguments streaming in), Executing (waiting for user response), and Complete (user has responded). Your render component receives the appropriate props for each state.respond callback should only be called once per tool invocation. Calling it resolves the tool call promise with the provided value.useFrontendTool: Under the hood, this hook wraps useFrontendTool with an internally generated handler, so all the same lifecycle behavior (mount/unmount registration, duplicate warnings) applies.void.useFrontendTool -- for tools with automated handlers that do not require user interactionuseRenderToolCall -- for rendering backend tool callsToolCallStatus -- the status enum used across tool hooksThe ToolCallStatus enum is exported from @copilotkit/react-core/v2 and defines the three phases of tool execution:
| Value | Description |
|---|---|
ToolCallStatus.InProgress | Arguments are being streamed from the agent. The tool has not started executing yet. |
ToolCallStatus.Executing | Arguments are fully resolved. For useHumanInTheLoop, the respond callback is available. |
ToolCallStatus.Complete | Execution is finished. The result string is available. |