An Approve button leaves an important question: what exactly did the person approve?
I’m building an owner-operated, self-hosted n8n system with AI assistance. Research and drafting feed a review queue; customer emails require my approval. This is my own project, not a paid-client case study.
The flow in the screenshot
The canvas shows a pending-draft lookup, duplicate-contact checks, a Gmail approval request, and an explicit approval branch. Before sending, it re-reads the approved row and verifies the approved snapshot. Follow-ups have a fresh thread check, and the sending path records a receipt. Declined/expired approvals and dry runs have separate branches.
Those are implementation steps, not proof that every failure case is solved. In particular, a send receipt is not delivery evidence.
Six scenarios I would include in an approval-workflow test plan
| Scenario |
Expected behavior to verify |
| Recipient changes after approval |
Stop and request fresh approval |
| Message changes after approval |
Stop and request fresh approval |
| Approval link is clicked twice |
Do not create a second send |
| Approval expires |
Do not send |
| Execution stops around the send step |
Reconcile the outcome before retrying; do not blindly resend |
| Recipient replies before a follow-up |
Block that queued follow-up |
Use synthetic contacts or a mailbox you control for these tests. For each case, record the input, expected result, actual result and execution evidence. This is a proposed checklist, not a claim that all six tests have passed in my project.
The part I’m especially interested in
A local “Sending” status cannot by itself prove what happened at the email provider if the execution stops at the wrong moment.
How do you handle that uncertain outcome in your n8n email workflows—especially when the provider has no send-idempotency key? What evidence do you require before allowing a retry?
Screenshot above: my workflow canvas, with the approval branch enlarged. It illustrates the structure; it is not an execution trace.
For that uncertain-send case, I’d leave the attempt unresolved. A timeout or an old “Sending” row isn’t evidence that another send is safe.
If your sending path supports a custom MIME Message-ID, generate it before sending and save it with the approved snapshot and attempt ID. Gmail’s messages.list API supports rfc822msgid: searches. First test in a mailbox you control that your sending path preserves the header; the n8n node’s capabilities matter here.
On recovery, search the sending account’s Sent mail for that ID, fetch the match, and compare its recipients and content with the approved snapshot. A confirmed match lets you record Gmail’s message ID and close the attempt without resending. That’s evidence of the sent message, not inbox delivery. No match leaves the outcome unknown.
I’d allow an automatic retry only with reliable evidence that the attempt failed before submission, or that the provider explicitly rejected it without accepting the message. Otherwise, hold it for review. If an operator chooses to resend despite uncertainty, record that decision and the duplicate risk rather than quietly treating it as a normal retry.
The custom Message-ID helps correlate evidence; it doesn’t make sending idempotent. Keep that unresolved attempt blocked from another worker or approval click.
Your crash test should cover Gmail accepting the message but the workflow losing the response. Recovery should find the existing message or stop for review.
Are you sending through the Gmail node, SMTP, or a raw Gmail API request?
2 個讚
This is a good example of why recording a successful workflow execution isn’t enough to guarantee reliable delivery.
I’d separate the send operation into three states:
PENDING: The email is approved but sending hasn’t started.
SENDING: A worker has claimed the send attempt.
SENT: The provider has acknowledged the request and the result has been recorded.
The difficult case is when the provider accepts the email but the workflow crashes before updating the database.
I wouldn’t automatically retry that attempt. I’d mark the outcome as uncertain and reconcile it using whatever evidence the email provider exposes.
If the provider supports an idempotency key, use it. Otherwise, retain a unique internal attempt ID, recipient, message fingerprint, timestamp, and provider response when available.
If reconciliation can’t establish the outcome, manual review is safer than potentially sending twice.
One question: Are you sending through Gmail directly or through a transactional email API? The available recovery mechanisms could be quite different.
Thanks @Marchik and @Mukarram_Ali1. I’m sending through n8n’s Gmail node using Gmail OAuth, rather than SMTP.
The current path reserves the attempt before sending and records Gmail’s message/thread IDs afterward. I haven’t verified that this path preserves a custom MIME Message-ID, so I can’t claim reliable recovery when Gmail accepts a message but the workflow loses the response.
For that unresolved case, I agree the attempt should remain blocked from automatic resend. A useful next test is to send only to a mailbox I control, interrupt around the receipt step, and check what evidence can be recovered.
My latest synthetic tests cover model fallback and holding insufficiently supported drafts for review. They don’t prove uncertain-send reconciliation.
Have either of you verified custom Message-ID preservation with the native Gmail node, or did you use a raw Gmail API request?
I like the idea of checking whether the email has changed after approval. Sometimes, even a small change to the recipient or message could cause problems. I think having a preview of the exact email before approving it would also be useful, so the user knows exactly what will be sent. The challenge is handling situations where the workflow stops during sending and you’re unsure whether the email actually went through.