The retried webhook that created a duplicate deal

Why the identity of an enquiry must survive retries and CRM outages.

A repeated webhook created a second CRM deal in the first version of my lead-intake reference build. Each execution could read the message, classify it and create a record. The failure was in the relationship between executions: the second delivery represented work that had already been requested, but the CRM write treated it as a new enquiry.

The fix in the reference design was an idempotency key based on the source and original timestamp. That key travels with the enquiry through processing and recovery. A retry must retain the identity of the original event. If recovery generates a fresh identity, the system has lost the information it needs to distinguish replay from a genuinely new request.

Why email alone is the wrong identity

An email address identifies a contact, within the limits of the source data. It does not identify every piece of work that contact may ask for. One person can submit several requests. Using only an email lookup to suppress duplicates would risk collapsing a later, legitimate enquiry into an earlier deal.

This gives the acceptance examples two separate jobs. Deliver the same source event again and check that there is no second deal. Then submit a distinct enquiry from the same contact and check that it remains available as separate work. A test that checks only repeated email addresses cannot tell whether the deduplication rule preserves both behaviours.

Source and timestamp also need an agreed meaning. The timestamp is the original event time, preserved on replay, rather than the time of each retry. The source identifies the intake origin used by the workflow. The case describes that design; it does not claim that an arbitrary combination of two strings is universally collision-free. A real integration needs its source identity and storage behaviour checked against the agreed examples.

Put the key before the CRM write

The intake flow normalizes the channel, contact, message, attachments, timestamp and source before qualification. The model then returns a structured result, including the request type, priority, summary and reply draft. An uncertain result goes to manual review. The CRM write uses the event identity regardless of whether the model produced a useful enrichment.

The model’s wording should not decide whether the event is new. Repeating qualification can produce a slightly different summary for the same message. A missing budget can remain null, while the request is still the same request. Keeping the original event identity separate from generated text makes that boundary inspectable in the execution log.

The reference case records one measured path from arrival to structured result at 7.3 seconds. That single timing is useful for describing the run. It does not prove duplicate handling, because a fast successful execution does not exercise a retry. The failure path needs its own input and its own inspection of the resulting CRM records.

Recovery is part of the process

When the CRM is unavailable, the enquiry enters a queue and is replayed after recovery. The queued item must retain the original data and idempotency key. Before replay, check whether the write might already have succeeded. After replay, check the actual CRM record instead of relying only on an execution marked successful.

A timeout deserves particular care in an acceptance scenario. The caller may not have received a response even though the receiving service completed the write. Retrying without preserving identity could turn that uncertainty into a duplicate. The desired behaviour is defined at the record level: the original enquiry remains actionable and the repeated delivery does not create another deal.

The reference build also separates model failure from CRM failure. If the model is unavailable or returns invalid output, it makes three attempts with exponential delay, then retains the original enquiry without enrichment and marks it for review. If the CRM itself is unavailable, the queue handles that outage. Neither path should silently discard the source message.

Leave enough evidence to investigate

The owner needs to inspect the input, model response, branch decision and write result. Those logs connect a queued item to its eventual record and make it possible to explain why a request was reviewed or replayed. Credentials stay in stored credentials, and access is limited to the object and permissions required by the integration.

The lead-intake case describes the original duplicate and the recovery design. The sample runbook carries the same rule into operating instructions: inspect the queued record, preserve its key, replay through the configured recovery path, and verify the result in the CRM.