In this piece
Odoo + n8n: retry order creation without duplicates
Suppose Odoo saved the order. The response never reached n8n. An operator sees a failed execution and presses retry. Your recovery design now decides whether the customer receives one order or two.
Make Odoo order creation repeatable by assigning one durable identity to the intended operation, enforcing that identity where the record is written, and reconciling the target after an uncertain response. Keep the same identity and payload on retries. Stop changed payloads or unresolved outcomes for review instead of blindly creating another record.
Idempotency means repeating an operation leaves the same intended business result. That property belongs to the operation you have implemented; a retry setting cannot supply it on its own.
Research checked: 5 November 2026. Product facts use official Odoo 20 and n8n sources. We executed ten synthetic Python/SQLite cases, including a deliberately discarded response after a local commit. We did not run Odoo, n8n or PostgreSQL, interrupt a live network, or measure a customer integration.
Why can a failed n8n execution leave an Odoo record behind?
A failed n8n execution can leave an Odoo record behind because the target may finish its write before the caller receives the result. Treat a lost response as an unknown outcome until evidence resolves it. A failure indicator describes the observed execution; it does not prove that every earlier business effect was rolled back.
In our example, the shop requests an order for company A with source reference SO-77 and an amount of EUR 125.00. The target creates its record, then the response is lost. Repeating an ordinary create request can produce another record for the same purchase. The second request may return successfully, leaving the operator with a green execution and a duplicate.
n8n’s Retry On Fail documentation describes automatic retries and the wait between attempts. Those controls govern when another attempt happens. Your receiving operation must decide what a repeated attempt means.
The opposite failure matters too. Suppose the workflow writes “processed” before creating the order, then stops between those steps. A replay sees that marker and skips the work. There is no duplicate, but there is no order either. Track receipt separately from completion, and inspect the final target record before closing an incident.
What should identify one intended Odoo order creation?
Identify an intended Odoo order creation with a durable business key scoped to its source and owning company. Preserve the approved payload alongside it. Delivery IDs and execution IDs identify attempts; they do not necessarily identify the purchase those attempts should create. Decide how a legitimate revision differs from a replay before enabling automatic retries.
Use a contract such as the following. These names are our proposed integration fields, not built-in Odoo fields or a promise that Odoo recognizes a particular HTTP header.
| Field | Example | Purpose |
|---|---|---|
| Source and company | shop, A | Keep independent business owners in separate namespaces. |
| Source business reference | SO-77 | Identify the purchase across deliveries and manual replays. |
| Command and revision | create_draft_order, 1 | Distinguish this creation from a later approved amendment or another business action. |
| Payload fingerprint | Digest of the agreed business fields | Reject reuse of the key with changed content. |
| Attempt references | Webhook delivery and n8n execution IDs | Trace individual attempts without making each attempt a new purchase. |
| Target evidence | Odoo record ID, company and accepted state | Show which business record fulfilled the operation. |
n8n’s idempotency guide notes that manual retries or retriggers receive a new execution.id and recommends input-derived keys across runs. Keep the business identity in durable storage before relying on it for recovery.
Specify which fields the fingerprint covers. An amount or currency change should not disappear behind a matching order number. Exclude delivery metadata that legitimately changes on every attempt. Normalize only according to the agreed contract: stripping leading zeroes from a customer identifier may change its meaning.
In the fixture, replaying EUR 125.00 returns the original record. Reusing the same key with EUR 130.00 raises a payload conflict and leaves the original amount intact. A reviewer must decide whether the later input is a correction, a new approved revision or an upstream defect.
Where must duplicate prevention be enforced?
Enforce duplicate prevention at the boundary that commits the business record. A receiving operation needs to claim the scoped key and commit its result consistently with the write. A separate n8n receipt table can coordinate attempts, but it cannot by itself make a later Odoo write part of the same database transaction.
The official JSON-2 transaction guidance says calls run in separate transactions and related operations requiring one transaction should execute through one server method. The existing Odoo API architecture guide covers the hosting and protocol constraints that determine which implementation is available.
A custom receiving method could validate the current caller, inspect an operation receipt, create the permitted record when needed, and save its result in one transaction. It must also resolve simultaneous requests safely. Merely searching for SO-77 before creating it leaves a gap: two workers can both search before either writes.
The n8n Odoo node reference lists Custom Resource CRUD operations. That list does not establish that a create operation implements your receipt contract. Verify the installed node and receiving method. Sending an Idempotency-Key header only helps when the receiver implements it.
Where you cannot implement or verify a destination contract, document the weaker guarantee. Controlled serialization and destination lookups can reduce uncertainty, but every writer and replay path must participate. Leave an unresolved write for investigation when the evidence cannot distinguish “not created” from “created but not yet found.”
External effects need their own boundaries. A receipt committed with an order does not prove that a later email was sent once. The email duplicate-send recovery guide examines that separate operation; do not silently include sending or payment in a promise about order creation.
What did the ten local failure cases show?
The ten local cases showed that duplicate prevention and completion recovery require different evidence. The fixture reproduced both duplicate records and lost work, then recovered the original record through a target-side atomic receipt. Every expected observation matched. These results describe our Python/SQLite model, including its locking behavior, and do not establish the behavior of Odoo or n8n.
The target model contains an order table and an operation-receipt table in one SQLite database. Its successful path writes them in the same transaction. A failure hook deliberately discards the response after commit; another stops before commit. The concurrent case starts two local workers, while the unsafe search case uses a documented interleaving of reads and writes.
| Case | Expected observation | Observed |
|---|---|---|
| Blind retry after the first create committed | Two records for one intended creation | 2 records |
| Processed marker saved before the effect, then interruption | Replay skips missing work | 0 records |
| Two searches complete before either unprotected create | Both attempts create | 2 records |
| Atomic target receipt, response lost after commit | Replay returns the original draft record | 1 record; same ID |
| Same operation key, changed amount | Conflict; original record remains unchanged | Conflict; EUR 125.00 retained |
| Same source reference in separately authorized companies A and B | One independent record per company | 2 records in separate namespaces |
| Replay after authority for company A is removed | Current authorization blocks access | Denied |
| Failure before the atomic transaction commits | No committed record; retry creates one | 0 before retry; 1 afterward |
| Two simultaneous requests to the model’s protected target | Both resolve to the same record | 1 record; same ID |
| Client receipt exists, target lookup is inconclusive | Retain an unknown outcome | unknown |
Download the runnable retry model and recorded model results. Save the Python file locally and run:
python3 retry-model.pyThe script uses temporary local databases and no network. It is not an Odoo module. The model’s draft state is its own field; the run did not create a quotation, reserve stock, post accounting or send a message in Odoo. Use the example to challenge the design, then run acceptance checks against the real receiving operation.
How should the operator recover an unknown result?
Recover an unknown result by matching the intended operation to evidence at the destination before choosing another write. Recheck the caller’s current authority and the payload revision. Preserve the original key throughout the investigation. If the lookup cannot establish what happened, retain UNKNOWN and assign an owner instead of turning absence of evidence into a successful replay.
| Evidence | Decision | Next action |
|---|---|---|
| One matching target result with the accepted company and content | Creation completed | Store the target reference and continue only with separately authorized downstream work. |
| Same key with changed business content | Conflict | Review the proposed revision; do not overwrite the original result during replay. |
| More than one target match | Duplicate incident | Stop new writes for the operation and investigate dependent records before correcting anything. |
| Current caller no longer has authority | Blocked | Escalate through the approved owner without reusing elevated credentials. |
| No conclusive destination result | Unknown | Use a verified repeatable receiving operation, or investigate manually before another create. |
“Creation completed” is an integration outcome, not an instruction to confirm a quotation or post an invoice. Keep the receipt’s lifecycle separate from the business record’s state.
An n8n error workflow is configured for a workflow and starts with Error Trigger. Use it to route the operation key and destination evidence to the recovery owner. A retry queue needs bounded attempts and a visible unresolved state; alerting alone does not repair the write.
Record the lookup scope. Searching under the wrong company or a narrower user role can return no visible match even when a record exists. An operator needs the source reference, exact target identity and evidence collected under an authorized context. The authenticated-action verification guide covers the equivalent evidence problem for browser actions.
Which acceptance evidence is required in Odoo and n8n?
Accept the Odoo and n8n integration only after testing repeated commands on its actual configuration and inspecting the resulting business records. Include response loss after a committed write and simultaneous attempts, with both permitted and denied users. The required outcome is the intended record in the intended state, with no unauthorized or unexplained additional effects.
Use the proposed production acceptance cases. Every case starts as NOT_RUN. Record the Odoo version and module revision, n8n version and node settings, test identities, approved input, target query and observed result. A workflow screenshot is useful supporting evidence, but it cannot replace the record comparison.
Test the recovery paths your team will use: automatic retry, operator replay after a workflow edit, and replay after a restarted worker. Include a changed amount under the old key and a legitimate second company using the same source reference. Verify that a revoked identity cannot recover protected details merely because an earlier attempt succeeded.
For invoicing, name the intended state explicitly. Odoo’s invoice documentation distinguishes Draft creation from confirmation to Posted, which creates the journal entry. A returned invoice ID does not prove posting or accounting approval. Test any subsequent confirmation as its own authorized business command.
Run the rehearsal in an isolated environment with controlled external services. A duplicate-payment or email case needs a safe substitute for the real recipient. Finance and operations should approve their respective business outcomes; a developer’s assertion that the node succeeded cannot settle them.
When should automatic retries remain disabled?
Keep automatic retries for uncertain writes disabled until the receiving operation has a verified repeatability contract and an owner can reconcile its result. Reads or explicitly repeatable commands can follow their own bounded policy. The decision should change when your actual integration passes the relevant failure cases, with evidence of both completed work and denied operations.
We would start with one order-creation command and its acceptance record. Add downstream confirmation or sending only after its separate recovery boundary is specified. Agree the expected records through the discovery and requirements process, including who resolves an unknown outcome after business hours.
Wavect’s API integration service is the commercial route for implementing that record-transfer boundary. Bring one failing execution and its target evidence to the discussion. The useful first deliverable is a reproducible recovery case with a clear release decision.
