Start with the intended action.
An integration can receive the same event more than once. It can also receive new information before an older action runs, or lose a response after an external system has already done the work.
The useful question is whether this particular action is still needed, permitted, and known to be incomplete. An event identifier, a current data revision, and a record of the attempt answer different parts of that question.
This guide proposes a design pattern and demonstrates it with a small local model. It is not an n8n workflow export or a description of automatic guarantees provided by GoHighLevel.
Know which record you are changing.
Choose a system of record for each field and store the identifiers needed to update the intended contact and opportunity. An email address, an event identifier, and an opportunity identifier serve different purposes.
HighLevel’s contact upsert follows the location’s duplicate contact configuration and field matching priority. That behavior needs to be checked for the account in use. Finding the existing contact does not make a later message or opportunity action safe to repeat. See the HighLevel upsert documentation.
Give an intended action a stable identifier that survives retries. If each attempt receives a fresh identifier, a completed action can look new to the workflow. Keep account and record boundaries part of the real implementation’s key.
Let new information change the plan.
A queued action should refer to the information it was prepared from. Before dispatch, compare that reference with the current record and check whether a person has taken ownership.
In this example, a revision is a positive integer assigned by an authoritative source. It is an explicit teaching assumption, not a field we assume HighLevel sends with every webhook. In a real integration, define an ordering or reconciliation strategy supported by the actual source.
If the newer update is already present, an action prepared from the old revision is held. The workflow must reassess it using current information. The model also prevents an older data update from replacing a newer revision.
A missing response is not a clean failure.
A request can reach its destination even if the caller never receives the response. Repeating a customer message or creating another opportunity at that point may repeat an action that already succeeded.
n8n provides retry settings for HTTP requests, including attempt limits and delays. The choice to repeat a particular side effect still belongs in the integration design. See the official n8n HTTP Request documentation.
Record an uncertain outcome separately from a known failure. Reconcile against trustworthy destination evidence, or ask an operator to resolve it. The example conservatively holds new attempts while an earlier one is unresolved. A matching confirmation resolves that attempt without resending it.
Step through the decision.
Choose a situation and advance the events. Watch the data revision, attempt count, ownership, and delivery state. Each scenario starts from a fresh in memory record.
For example, a repeated input followed by the same confirmed action leaves the attempt count at one. When confirmation is missing, even a new action identifier is held. Human takeover blocks a queued action, but cannot recall an action already dispatched.
Synthetic events. Nothing is sent or saved.
- Data revision
- 0
- Attempts
- 0
- Owner
- Automation
- Action
- No attempt
Choose a scenario, then advance one event at a time.
A small model you can inspect.
The example and its test file use synthetic identifiers and make no network requests. The same model drives the interactive view above and the downloadable checks.
The standalone workflow reliability example on GitHub includes the source, setup instructions, five scenarios and thirteen behavior checks. It is a teaching model, not an n8n workflow export or a production integration.
Checks run on 7 September 2026 with Node.js 26.0.0 cover ordinary and repeated events, stale revisions, matching and unrelated confirmations, uncertain completion, human takeover, and unchanged earlier snapshots. A separate check verifies the outcome of every displayed scenario.
Save both files in the same folder. With Node.js 24.2 or newer, run:
node workflow-state.mjs
node --test workflow-state.test.mjsWhat a real integration still needs.
- Durable and scoped state
- The model stores one record in memory. A deployed system needs persistent state, account boundaries, stable action identifiers, and a retention policy.
- Concurrent execution control
- The example processes events in sequence. Real workers need atomic claims or equivalent coordination, plus a way to reconcile interrupted work. An in memory check cannot protect separate workers.
- Actual authorization and consent
- The model’s human owner is one teaching flag. Real permission checks, communication preferences, consent, opt outs, and approved content need their own enforcement.
- Destination evidence
- The confirm event is synthetic. A real system must define which response, record, or receipt proves completion, and how that evidence is matched to the attempt.
- Failure and recovery testing
- Test the actual APIs, credentials, timeouts, rate limits, and crash windows. A browser illustration or passing model test cannot establish production readiness.
Apply it to the actual workflow.
Start with one repeated task, the fields each tool owns, and the external actions it can trigger. Then agree what should happen when the event repeats, information changes, or the result cannot be confirmed.
The website to CRM case describes a separate historical implementation. For help assessing or building your own connection, explore GoHighLevel integration services or n8n automation and repairs.
Questions.
Why does n8n run the same action twice?
Usually because the same event arrived twice, a retry repeated a request whose first attempt actually succeeded, or two executions handled the same record at once. Use stable action identifiers, atomic coordination of attempts, provider idempotency where supported, and reconciliation of uncertain outcomes. A separate lookup before a write can race.
Does GoHighLevel deduplicate contacts for me?
The contact upsert follows the location’s duplicate settings and field matching priority, so it can find an existing contact. It does not stop a workflow from sending a second message or creating a second opportunity for that contact.
What should happen when the response never arrives?
Record the attempt as uncertain, hold new attempts for that action, and reconcile against destination evidence or an operator decision. Treating a timeout as a failure and retrying can repeat an action that already happened.
Written by Younes Nadif · 7 September 2026 · Updated 14 September 2026
