Example: an agent that asks before it sends
The agent writes, a person approves, only then does mail leave. Drafts are the whole mechanism.
An agent that can send mail unsupervised is a liability in most businesses. The usual answer is to make the agent worse — shorter leash, narrower prompts. The better answer is to keep the agent as capable as it is and put a person between it and the outside world.
Mail that has left cannot be recalled. A draft can be deleted.
What it does
- Mail arrives
- The agent writes a draft — nothing leaves
- A person reads it and either sends or bins it
1. The agent writes a draft
Identical to the auto-reply example, with one change: create a draft instead of replying.
Python
draft = carly.post(
f"/v0/inboxes/{message['inbox_id']}/drafts",
json={
"to": [message["from"]],
"subject": f"Re: {message.get('subject') or ''}",
"text": answer(question),
# Threading is carried by the draft, so when it is eventually sent it
# lands in the same conversation rather than starting a new one.
"in_reply_to": message["message_id"],
# Your own id for this draft. Sending the same one twice is then a
# no-op rather than two emails.
"client_id": f"reply-to-{message['message_id']}",
},
).json()
Nothing has been sent. The draft sits in the inbox until somebody acts on it.
2. A person reviews it
In the console, or from a terminal:
carlyemail drafts support@carlyemail.com
Re: Order #4821 hasn't arrived to sam@gmail.com
dft_00ms9cgs8ixuef7g7l0yu6kd
3. They send it, or they don't
carlyemail send-draft support@carlyemail.com dft_00ms9cgs8ixuef7g7l0yu6kd
Or delete it:
curl -X DELETE \
"https://api.carlyemail.com/v0/inboxes/support@carlyemail.com/drafts/dft_00ms…" \
-H "authorization: Bearer $CARLYEMAIL_API_KEY"
Making the approval step somebody's job
A queue nobody looks at is not a review step. Two ways to make it real:
Notify a human when a draft appears. Subscribe to draft.created and post it
into Slack, or mail it to yourself — you are, after all, holding an email API.
Give the agent a key that cannot send. This is the stronger version, because
it does not depend on anybody's discipline. Issue an inbox-scoped key that grants
draft_create but not message_send:
curl -X POST https://api.carlyemail.com/v0/inboxes/support@carlyemail.com/api-keys \
-H "authorization: Bearer $CARLYEMAIL_API_KEY" \
-H 'content-type: application/json' \
-d '{"name": "drafting agent",
"permissions": {"message_read": true, "draft_create": true}}'
Permissions are a whitelist, never a merge: the moment a key carries any explicit
permission, that object is the complete grant and everything absent is denied. So
that key can read mail and write drafts, and calling send with it returns 403
regardless of what the agent decides it would like to do.
That turns "the agent is supposed to ask first" into "the agent cannot do otherwise", which is a different kind of guarantee.
Where to go next
- Drafts — scheduling, attachments, editing before sending
- Authentication — the full permission list and how scoping works
- Auto-reply — the same loop without the approval step