Human-in-the-Loop: Approving Agent Actions Before They Run
Reading data is low-risk. Sending emails, moving money or deleting records is not. A human-in-the-loop step pauses the agent and asks for approval before risky tools run.
Classify your tools
- Read-only (search, get_*): run automatically.
- Reversible writes (draft, add_to_cart): run automatically, log them.
- Irreversible or external (send_email, charge_card, delete_*): require approval.
Gate inside the tool
The simplest pattern keeps the normal loop and makes the tool itself ask for approval. If the human says no, return that as the tool result so the model can adjust.
const sendEmail = betaZodTool({
name: 'send_email',
description: 'Send an email to a customer. A human must approve each email.',
inputSchema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
run: async (email) => {
const decision = await askHuman(`Send this email to ${email.to}?`, email);
if (!decision.approved) {
return `The user declined to send this email. Reason: ${decision.note || 'none given'}`;
}
await mailer.send(email);
return 'Email sent.';
},
});
Asynchronous approvals
In a web app the human may answer minutes later. Store the conversation (the messages array) and the pending tool_use in your database, show an approval card in the UI, and resume the loop with the tool_result once they decide.
// On approval
const state = await db.agentRuns.get(runId);
state.messages.push({
role: 'user',
content: [{ type: 'tool_result', tool_use_id: state.pendingToolUseId, content: resultText }],
});
await continueAgent(state);
Make approval easy to judge
- Show exactly what will happen: recipient, amount, records affected.
- Let the human edit the arguments, not just approve or reject.
- Log who approved what and when.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.