April 24, 2026 · 6 min read
Human-in-the-loop: approve before your agent hits send
Giving your AI agent an email address is a quietly big step. It means your agent can now reach your customers, your vendors, your boss — the same people you reach. That's the point. It's also the part that makes even the most bullish builder hesitate the first time.
We heard a version of the same question from early beta users:
I want my agent to send email. I don't want my agent to send that email.
Today we're shipping the answer: human-in-the-loop approval. It's a per-agent switch that holds outbound messages until you approve them — and adds almost nothing to the rest of the flow when it's off.
What gets held
When an agent has HITL on, every outbound path pauses:
- Fresh sends from
POST /api/v1/send - Replies to inbound mail from
POST /api/v1/.../reply - Test sends from the dashboard
The API still accepts the request — it just responds 202 Accepted with status: "pending_approval" instead of the usual 200 OK + "sent". The message lands in a holding table with a TTL; your SDK call returns immediately so the rest of your agent's logic doesn't block.
Nothing changes for agents that don't have HITL on. Same 200, same SES delivery, same latency.
How you approve
You get three entry points.
The email
As soon as a message lands in pending state, the platform emails the account owner. The email carries the subject, recipient list, and expiration time — but not the body. Sensitive draft content never leaves our systems by email; it only appears on a token-gated review page.
The email has two buttons: Review & approve and Review & reject.
The confirmation page
Clicking a button takes you to a one-page, no-JavaScript confirmation view. Full body, recipients, subject, attachments — plus a single button to commit the action. The "click the link in the email, see a form, click the button" split protects you from Gmail, Outlook Safe Links, or corporate mail gateways that preview URLs (they'd see the page, not the side effect). The token is single-action (an approve token can't be redeemed at reject) and short-lived.
The dashboard
Or skip the email entirely. Go to Dashboard → Pending:
- List view sorted by soonest-expiring first
- Click any row to open the edit form
- Change subject, recipients, body, HTML — whatever
- Approve with edits (the server records
edited: trueso you have an audit trail) or reject with an optional reason
Dashboard edits go through the same POST /api/v1/messages/{id}/approve endpoint as everything else; edits are sent as a field-diff so the edited flag only flips when you actually changed something.
The CLI
Prefer a terminal?
# list what's waiting
e2a pending list
# inspect one
e2a pending show msg_abc123
# approve as-is
e2a pending approve msg_abc123
# approve with edits — opens $EDITOR
e2a pending approve msg_abc123 --edit
# reject with a reason
e2a pending reject msg_def456 --reason "wrong tone"
Same permissions, same endpoints, same audit trail.
Turning it on
One command:
e2a agents update my-agent --hitl \
--hitl-ttl 3600 \
--hitl-expiration-action reject
--hitl-ttl sets how long a message stays pending before the platform finalizes it automatically (capped at 7 days). --hitl-expiration-action decides what that finalization looks like:
reject— after TTL, the message is dropped. Safest default; matches "if I don't look in an hour, assume I didn't want this sent."approve— after TTL, the message goes out automatically. Useful fore2a agents update my-agent --hitl --hitl-ttl 30 --hitl-expiration-action approve— a 30-second review window gives you a "reject undo" but keeps the agent responsive.
Or toggle from the dashboard: Agents → (your agent) → HITL → Edit.
What happens if you ignore it
A background worker sweeps every minute. It looks for rows whose approval_expires_at is in the past and applies the agent's configured expiration action. Rows hit the state you chose (expired_approved or expired_rejected) and their body is scrubbed from storage. Multi-instance deploys use FOR UPDATE SKIP LOCKED so two workers can't race on the same row.
If the auto-approve send itself fails (SES down, for example), the row transitions to expired_rejected with the error recorded as the rejection reason rather than looping forever.
Retention, concretely
Enabling HITL stores the full composed message — subject, body, HTML, attachments — in Postgres up to hitl_ttl_seconds. On every terminal transition (sent, rejected, expired_*) the server immediately scrubs those columns to NULL. Headers and metadata stay for the normal 30-day message-TTL window; bodies do not.
TTL is server-capped at 604800 seconds (7 days) so the maximum exposure is bounded regardless of agent config. And, again: the notification email carries recipients and subject only. Body content stays server-side, behind the token-gated confirmation page.
What I'd use it for today
- First deploy of any new agent. Turn HITL on with a 7-day TTL, auto-reject on expiry. Every outbound gets your eyes before going out. Once you've approved enough to trust the behavior, shorten the TTL or turn it off.
- High-stakes replies. A support agent might auto-reply to 95% of inbound with HITL off, but route "anything mentioning refunds" through a higher-care path with HITL on.
- Weekend / overnight review. Set a 12-hour TTL with auto-approve so your agent stays responsive but anything truly off-brand gets caught if you check email before the window closes.
- Staging vs production. Your dev environment runs with HITL on; prod runs with it off once the behavior is proven.
The API surface
Every action is mirrored across the dashboard, CLI, and API. If you want to build your own approval UI:
GET /api/v1/messages?status=pending_approval— list pending across all your agentsGET /api/v1/messages/{id}— full detail (body included while pending)POST /api/v1/messages/{id}/approve— optional body overridesPOST /api/v1/messages/{id}/reject— optional reasonPUT /api/v1/agents/{email}— toggle HITL on/off + configure TTL and expiration action
Full spec in the API reference.
Try it
Off by default. Toggle it on one agent, send a message to yourself, and watch the flow end-to-end:
# from a fresh shell with the CLI installed
e2a agents update my-agent --hitl --hitl-ttl 3600
e2a send --to you@yourdomain.com --subject "test" --body "anyone home?"
# → Holding for approval: msg_abc123
Check your inbox for the approval email. Click through, look at the confirmation page, approve. The message shows up in the recipient's inbox a second later.
Then turn it off and watch the same send command return sent in one shot.
That's the whole feature. Per-agent, reversible, opt-in.
If you've been holding back on letting your agent send real mail to real people — this is the switch. Enable it on an agent and let us know how it goes.