Email drafts
Sending email is external publication: ask before every send. handup shows the
human a rendered email preview, lets them edit the draft and leave feedback, and
records the decision. handup never sends mail and has no Gmail, IMAP or SMTP
access. After approval you send the message with your own tool (for example
gog gmail send, himalaya, or an MCP mail tool).
- Draft the email.
- Ask with an
emailpreview and the editable draft asinput({to, cc, bcc, subject, body}). Do not send while the request is pending. - Approved: send
decision.fieldsif present (the human edited the draft; it is the complete edited draft, not a patch), otherwise send your original draft. Send exactly that content once. Readfeedbackeither way. - Denied: do not send. Read the feedback, revise the draft and ask again with the revision. A denial is not permission to retry the same draft.
- Expired, cancelled or error: do not send.
Without input the request is review-only: the human can approve or deny and
leave feedback, but not edit.
Open in mail app. Email previews in the inbox and History have an “Open in
mail app” button that hands the shown draft (the human’s edit while editing)
to their own mail client as a mailto: link with To, Cc, Bcc, subject and
the plain-text body prefilled. The human may send it from there. Opening the
mail app changes nothing about the request: it stays pending until the human
approves or denies it here, and handup still never sends mail.
Preview shape
Section titled “Preview shape”{ "type": "email", "email": { "from": "Ari <ari@example.com>", "to": ["Dana Lee <dana@example.org>"], "cc": ["ops@example.org"], "bcc": [], "reply_to": "billing@example.com", "subject": "Re: Q3 invoice", "date": "Tue, 30 Sep 2026 16:05:00 +0000", "body": "Hi Dana,\n\nThe signed invoice is attached.\n\nBest,\nAri", "attachments": [{"name": "invoice-q3.pdf", "mime": "application/pdf", "size": 48213, "blob": "sha256:…"}], "in_reply_to": {"from": "Dana Lee <dana@example.org>", "date": "Tue, 30 Sep 2026 09:12:00 +0000", "body": "Could you send the signed invoice?"} }}bodyis Markdown (plain text renders as written, line breaks kept).body_htmlis optional; it renders only in the sandboxed HTML preview frame (network blocked), never inline. Edits changebodyonly: if the human edited the draft, regenerate or drop your HTML alternative.- Addresses are
addressorName <address>(the name may be quoted). Each address islocal@domainwith exactly one@; the local part is ASCII letters, digits and!#$%&'*+-/=?^_`{|}~.(at most 64, no leading, trailing or doubled dots); the domain has at least two labels and ends in a letters-only TLD of two or more letters or anxn--label (a@b,ops@example.org2are rejected; Unicode domains are fine). At least one ofto/cc/bccis set, and the subject or body is non-empty. - An approval’s
decision.fieldson an email request must be a complete draft{to, cc, bcc, subject, body}meeting the same rules; the API and CLI reject anything else with HTTP 400, so an approved edit is always sendable. - Attachments are shown as chips the human can open or download.
blobis a hash fromPOST /v1/blobs; the CLI and MCP also accept a localpathand upload it for you. Without either, the chip shows the name only. in_reply_tois the earlier message, shown collapsed under the body.
{"name": "request_approval", "arguments": { "title": "Send reply to Dana", "kind": "review", "risk": "medium", "previews": [{"type": "email", "email": { "from": "ari@example.com", "to": ["dana@example.org"], "subject": "Re: Q3 invoice", "body": "Hi Dana,\n\nInvoice attached.\n\nAri", "attachments": [{"path": "out/invoice-q3.pdf"}]}}], "input": {"to": ["dana@example.org"], "cc": [], "bcc": [], "subject": "Re: Q3 invoice", "body": "Hi Dana,\n\nInvoice attached.\n\nAri"}, "wait": false}}Wait with wait_requests. An approved reply looks like
{"status":"approved","option":"approve","feedback":null,"fields":{"to":[…],"cc":[],"bcc":[],"subject":"…","body":"…"}};
fields is absent or null when the human approved without editing.
--preview email:FILE (or email:- for stdin) reads the email JSON object
above. The CLI also uses the draft as input, so the human can always edit it;
for a review-only request send the full request with handup ask --request -
and omit input.
handup ask --title "Send reply to Dana" --risk medium \ --preview email:draft.json --wait --json > decision.jsoncase $? in 0) jq -e '.fields' decision.json >/dev/null && jq '.fields' decision.json > send.json \ || jq '{to, cc, bcc, subject, body}' draft.json > send.json # send send.json with your own mail tool, e.g. gog gmail send ;; 1) jq -r '.feedback // empty' decision.json # revise, then ask again ;; *) echo "not approved; do not send" ;;esacWithout --json, --wait prints the request, then any feedback, then
Edited input: followed by the edited draft when the human changed it. Exit
codes are the usual ones: 0 approved, 1 denied, 2 expired, 3 cancelled, 4 error.
handup demo seeds an editable email reply with an attachment to try the
review and Edit & approve flow.