The second click is the useful test
A visitor fills in the contact form, clicks submit and sees no immediate change. They click again. Or the form retries after a slow response. Both requests reach workflow 02.03 with the same email address and message. The expected result is one lead with one clear history. The current result is different: the Leads sheet receives a new row for every valid request.
That is not an edge case hidden deep in the workflow. It follows directly from the path. Form Submitted accepts the request. Validate Submission cleans and checks its fields. Valid? rejects malformed input. The model categorises the valid enquiry. Then Add To CRM uses the Google Sheets operation append. There is no lookup, match or duplicate branch before that write.
So what tells 02.03 that an entry is a repeat? Nothing does. The workflow has no repeat key. That answer is less flattering than inventing one, but it is the important fact about the file as shipped.
Validation is not duplicate detection
The validation step checks whether the email looks valid, the name and message are long enough, and a hidden spam field is empty. It also trims surrounding space and converts the email to a string. Those checks decide whether one submission is acceptable. They do not compare it with anything already stored.
This distinction matters because two individually valid submissions can still represent the same enquiry. The second request passes every existing test. The model runs again. The sheet gets another row. The workflow also sends another confirmation email and another Slack notification because those actions run in parallel after categorisation.
The Google Sheets node supports more than a blind append. Its official documentation lists Get Row(s), Update Row, and Append or Update Row alongside Append Row. The last of those can update a row when a matching row already exists. The current workflow simply does not use that operation. n8n documents the available Google Sheets operations here.
Choose what “the same” means
The safest first key for this workflow is not the complete message. It is a normalised email address. Before the lookup, trim whitespace and convert the address to lowercase. Then search the email column in Leads for that value. A match means the CRM already knows this contact; no match means a new row may be created.
That rule solves the named case: the same visitor sends the same form again under the same address. It also avoids treating Mara@example.com and mara@example.com as separate contacts. The comparison belongs before the model call, so a repeat does not spend another model request merely to discover that the contact already exists.
Email alone is not a universal identity. A shared office address can belong to several people. One person can use a private and a work address. And the same person may submit a genuinely new project later. Calling every repeated email a duplicate would throw new information away.
For a small lead sheet, “existing contact” is therefore a better branch name than “duplicate”. On a match, update lastContact, preserve the new message in notes or an activity log, and mark the event as a repeat submission. On no match, append the new lead. The workflow should combine records without pretending the second message never happened.
The repair in the workflow
The revised path needs only a few explicit decisions:
- Normalise the validated email with trim and lowercase.
- Look up that value in the Leads email column.
- If a row exists, update its contact time and retain the new message as activity.
- If no row exists, categorise the enquiry and append the lead.
- Send the visitor one response for this execution, but notify the owner differently for an update and a new lead.
n8n also has a Remove Duplicates node that can compare selected fields with values seen in previous executions. Its documentation says that previous-execution mode needs a unique value or field combination, and that the node keeps a bounded history. That makes it useful for suppressing repeated events. For a CRM, the sheet lookup is usually the clearer authority because it answers whether the contact exists in the actual business record, not only whether this workflow remembers seeing the event. The n8n Remove Duplicates documentation describes both modes.
A duplicate key is not enough under concurrency
There is one remaining failure. Two identical requests can arrive before either execution has written its row. Both lookups find nothing, and both executions append. This is the classic gap between checking and writing.
Google Sheets is convenient, but it is not a database constraint. If occasional cleanup is acceptable, the lookup-and-update design may be proportionate for a small form. If duplicates would trigger costly or irreversible work, use a store that can enforce a unique key or accept an idempotency key from the form. The write then becomes atomic: one key can create one record, while a repeated request updates or returns the existing result.
That is also where this workflow may not fit. A CRM with contact matching, activity history and unique-field rules will handle identity better than a spreadsheet assembled into a CRM. The sheet version stays understandable and editable, but that simplicity includes weaker guarantees.
What the current file proves
Workflow 02.03 does several useful things before and after the CRM write. It rejects malformed input, extracts structured context, records a lead, confirms receipt and alerts the owner. It has also been checked against a real n8n installation. It has not been run end to end with real form, Google Sheets, Gmail, Slack and model accounts, so credential behaviour and live retries still need test accounts.
On repeat handling, however, the file is unambiguous. It recognises a repeated entry by no field at all. Every valid execution reaches an append operation. The repair is to define the identity rule openly, normalise the email, look it up before enrichment, and branch between update and append. Then the second click becomes a recorded event instead of a second lead.