How to Implement Human-in-the-Loop for AI Emails
approved status.This approach transforms the AI workflow from a risky "fire and forget" system into a controlled pipeline: the agent drafts, a human audits/edits, and only then does the code execute the send command.
The Logic Flow
Instead of a direct call to an SMTP server, the agent follows this sequence:
1. Draft Generation: The agent produces the content.
2. External Holding: The draft is pushed to a review system (like Impri) via API.
3. Human Intervention: A reviewer reads the draft, makes necessary edits to the body or subject, and hits "Approve."
4. Execution: The system polls for the decision; if approved, it sends the final edited version, not the original AI draft.
Real-world Implementation: Node.js & TypeScript
Here is a practical tutorial on how to build this approval gate using nodemailer and a review API.
import nodemailer from "nodemailer";
const IMPRI_KEY = process.env.IMPRI_API_KEY!;
const IMPRI_BASE = "https://api.impri.dev";
async function sendWithApproval(opts: {
to: string;
subject: string;
body: string;
expiresIn?: number; // seconds; default 3600
}): Promise<"approved" | "rejected" | "expired"> {
// 1. Push the draft to review
const push = await fetch(`${IMPRI_BASE}/v1/actions`, {
method: "POST",
headers: {
Authorization: `Bearer ${IMPRI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
kind: "email.send",
title: `Email to ${opts.to}: ${opts.subject}`,
preview: {
format: "markdown",
body: `**To:** ${opts.to}\n**Subject:** ${opts.subject}\n\n---\n\n${opts.body}`,
},
editable: ["preview.body"],
expires_in: opts.expiresIn ?? 3600,
}),
});
if (!push.ok) throw new Error(`Push failed: ${push.status}`);
const { id: actionId } = await push.json();
// 2. Poll until human decision
let result: { status: string; decision?: { final_preview?: { body: string } } };
for (;;) {
const poll = await fetch(`${IMPRI_BASE}/v1/actions/${actionId}`, {
headers: { Authorization: `Bearer ${IMPRI_KEY}` },
});
result = await poll.json();
if (result.status !== "pending") break;
await new Promise((r) => setTimeout(r, 10_000));
}
if (result.status !== "approved") {
return result.status as "rejected" | "expired";
}
// 3. Send the human-approved version
const approvedBody = result.decision!.final_preview!.body;
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});
await transporter.sendMail({
from: process.env.SMTP_FROM,
to: opts.to,
subject: opts.subject,
text: approvedBody,
});
// 4. Update audit log
await fetch(`${IMPRI_BASE}/v1/actions/${actionId}/result`, {
method: "POST",
headers: {
Authorization: `Bearer ${IMPRI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "success" }),
});
return "approved";
}By treating human approval as a mandatory API response rather than a prompt suggestion, you create a fail-safe LLM agent deployment that is actually production-ready.