Retry a marketing automation without duplicate actions
Use a stable idempotency key and durable operation state to retry after a lost response without creating duplicate records or notifications.

What if the write succeeded but the response disappeared?
The difficult retry case is a successful operation whose acknowledgment never reaches the caller. The caller sees a timeout and retries. Without a stable identity and durable state, the receiver may create another inquiry, order or notification even though the user intended only one action.
n8n documents error workflows and retry-related execution metadata. Those features help route and inspect failures, but they do not establish exactly-once delivery. The message-state contract below is an application-level design that still needs tests against its actual database and downstream receiver. [Handle errors gracefully]
A message-state table for the ambiguous timeout
| Step | Durable receiver state | Caller behavior |
|---|---|---|
| First submit | No operation yet | Create request ID and retain it |
| Receiver commits | Inquiry saved with request ID; outbox queued | Wait for acknowledgment |
| Response lost | Inquiry remains saved | Treat outcome as unknown |
| Retry same ID and payload | Return existing receipt; no new inquiry | Display the confirmed receipt |
| Retry same ID, different payload | Reject conflict | Ask caller to resolve the changed operation |
| Notification worker retries | Same outbox item and delivery identity | Record outcome separately from inquiry save |
Generate the identifier once for the logical operation. If every network retry gets a new ID, the receiver cannot distinguish a retry from a new request. Conversely, reusing an old ID for a genuinely new inquiry can suppress legitimate work. Define the operation’s lifecycle and when a user action begins a new one.
A usable idempotency fixture
{
"requestId": "fixture-request-001",
"operation": "create-inquiry",
"payloadHash": "fixture-payload-v1",
"state": "saved",
"receiptId": "fixture-receipt-001",
"notification": "queued"
}The receiver should enforce uniqueness at the durable storage boundary and compare the normalized payload associated with the key. A preliminary “does this key exist?” lookup followed by a separate unprotected insert can race. Test two simultaneous requests, not just two sequential calls.
Keep request IDs and payload hashes out of public analytics when they can identify an operation or user. The operational store needs them for reconciliation; the reporting event can use a fixed event name and a bounded status instead. Diagnostic usefulness does not justify sending the entire form payload to telemetry.
Why should notifications have their own state?
Saving an inquiry and sending a notification are different operations. Commit the inquiry and an outbox item together, then let a worker deliver the notification. A temporary notification failure should not erase the saved inquiry or encourage the user to submit it again.
The destination may not support deduplication. If the notification service accepts a message but its acknowledgment is lost, a retry can still duplicate the message. Name that limitation explicitly and choose a reconciliation or receiver-side deduplication strategy where available. An application cannot promise exactly-once effects merely by calling its retry function idempotent.
- Simultaneous identical requests create one durable inquiry.
- Same key with changed payload produces a conflict.
- Lost acknowledgment returns the existing receipt on retry.
- Notification failure leaves inquiry status truthful.
- Worker restart does not forget queued or uncertain deliveries.
Record the effect, not just the HTTP status
Inspect durable record counts, receipts and outbox transitions after each fixture. HTTP 200 alone does not prove that only one record exists. A failed request alone does not prove that no record was written. The acceptance evidence should show the actual effect at the receiver and the caller’s recoverable state.
I would keep the unknown-outcome state visible until reconciliation completes. It is less tidy than labeling every timeout failed, but it matches the failure mode the retry design must handle.
Sources
AI-assisted writing and an AI-generated editorial cover. Source links and checked dates are listed below; illustrative examples are labeled in the text.
- Handle errors gracefully — checked 2026-10-18