Lorixen.

How to handle failed and duplicate automation events

Assume every event can fail and every event can arrive twice. Give each event a stable identity so a repeat is recognised and ignored, retry only actions that are safe to repeat, alert a named person when something fails, and write down the manual recovery step in plain words. Then test the failure paths on purpose rather than waiting for a real one.

Why duplicates and failures are normal

Networks drop, services time out and senders retry. A request that looks like it failed may in fact have worked, so retrying it blindly can create a second invoice, a second booking or a second email. Designing for this is routine rather than pessimistic.

Give each event a stable identity

The idea behind an idempotency key is that the sender attaches a unique key to a request, so the receiver can recognise a repeat. Stripe describes this for its API: if a connection error occurs you can repeat the request without risk of creating a second object or performing the update twice. Stripe saves the status code and body of the first request for a key and returns the same result for later requests with that key.

The same pattern works inside your own automations. Use an identifier that already belongs to the event, such as a booking number, as the key for a claim. A plain look up followed by a separate write is not enough on its own, because two copies of the event can both look, both find nothing and both carry on. The next sections explain how to close that gap and what it cannot cover.

Details worth copying

Stripe suggests random identifiers such as version 4 UUIDs, and advises against using sensitive data, such as email addresses, as keys. For API v1, it says keys can be removed after at least 24 hours, and that it compares the parameters of a repeated request with the original and returns an error if they differ. For API v1 it also notes that all POST requests accept keys, and that keys should not be sent on GET and DELETE requests because those are idempotent by definition. For API v1 it adds that if incoming parameters fail validation, or the request conflicts with another request executing concurrently, no idempotent result is saved and the request can be retried. These are Stripe specifics, so check the documentation of the service you actually use.

When two copies arrive at the same moment

Concurrent duplicates are the case a simple check misses. Imagine two copies of the same booking event reaching your workflow within a second. Each looks up the booking number in a list of confirmations sent, each finds it missing, and each sends an email. When checking, sending and recording are three separate steps, there is a gap in which both copies see the same old state.

Close the gap by claiming the event before acting. Make the claim a single step that can succeed for only one copy, for example inserting a row whose booking number must be unique, or a conditional update that changes a status from new to processing only if it is still new. The copy that wins the claim goes ahead. The copy that loses stops, or waits and reads the recorded outcome. Give the state a few plain values, such as claimed, sent, failed and outcome unknown, so a person can see where each event stands.

AWS describes the same idea on the service side. In the Amazon Builders' Library article on idempotent APIs, recording the idempotency token and every mutating operation made for the request must together meet the properties of an atomic, consistent, isolated and durable (ACID) operation, so the service never records the token without creating the resource, or creates the resource without recording the token. That applies to changes made inside the service's own transaction. It does not extend to an email sent through an outside provider, which no database transaction can undo or confirm. Treat the two as separate problems.

When the email send itself is uncertain

An atomic claim stops two copies from both proceeding. It does not tell you whether an email was delivered, and it does not on its own guarantee that no duplicate is ever sent. Suppose one copy wins the claim, calls the email service, and the connection times out. The message may have gone, may not have, or may be queued. Retrying blindly risks a second email, and doing nothing risks no email at all. AWS describes the same dilemma for a request that gets no response: the caller cannot tell whether the work happened, and may need a reconciliation step to find out.

So before calling the email service, durably record the state as send attempted, outcome unknown, and make sure that record is saved so it survives a crash. Change it to sent or failed only after the provider gives a clear answer. If the process crashes after the email goes out but before it is marked sent, the event stays visibly ambiguous and goes to reconciliation. It is never resent automatically. Treat that state as needing a person, and name who reconciles it, for example the owner or office manager. Their note should say where to check, such as the sent folder or delivery log of the email service, and whether the customer received the confirmation. Logs do not always settle the question: an absent entry does not prove nothing was delivered, and so does not make a resend safe. If the check cannot determine the outcome, leave the event unresolved with the named owner rather than resending. Only once the owner has established the outcome do they mark the event as sent, or resend once and record it. If your email service accepts a key of its own for sends, use it, but read its documentation first, because support and retention differ between services.

Retry only what is safe

Sorting actions into two groups makes decisions easier.

  • Safe to repeat: reading data, setting a status to a fixed value, saving the same record under the same identity.
  • Unsafe without protection: sending an email, charging a card, creating a new record, or adding to a running total.
  • For unsafe actions, claim the event's identity in one step that only one copy can win, and check for an earlier success before acting. A claim reduces duplicate attempts but does not prove that an outside action succeeded.
  • Space retries out and stop after a small number, so a broken service is not hammered.

Make failures visible

n8n lets you set an error workflow that runs when an execution fails, for instance to send an email or Slack alert. The error workflow starts with an Error Trigger node, and one error workflow can serve several workflows. A Stop and Error node lets you fail a run on purpose under conditions you choose. Whichever tool you use, make sure a failure reaches a named person rather than a log nobody reads.

Write the recovery step

For each failure type, write one sentence: what a person does next. Examples include re-sending a message from the log, marking a record as handled by hand, or contacting the customer. Include where to look and who decides. A recovery note that exists only in someone's head fails the first time they are on holiday.

A hypothetical example

Imagine a workflow that sends a confirmation email when a booking is saved. The booking system sometimes calls the workflow twice, occasionally almost at the same moment. The workflow first claims the booking number in a confirmations table where each number must be unique. Only the copy that wins the claim goes on to send, and it then marks the booking as sent. The other copy stops. If the email service is down and rejects the message, the error workflow alerts the owner, who follows a note saying to resend from the list of unconfirmed bookings. If a send times out with no clear answer, the booking stays marked outcome unknown and the owner checks the email service's delivery log before resending or marking it done. This example is hypothetical.

Sources

See the guides for business owners

WhatsAppLet’s talk