Integration
The dangerous ERP write is the one that succeeds
Released interfaces, idempotency keys and read-back assertions — and the write that succeeds against a deprecated endpoint while every dashboard stays green.
- Published
- Reading
- 8 min
- Based on
- SAP clean-core doctrine, published vendor platform limits (Temporal, Camunda 8, Power Automate, n8n) and integration practice; method rather than a delivered ERP integration — see the boundary at the end
A payment run posts 412 supplier invoices. The connection drops on number 287. Somebody restarts the job, and 126 invoices post a second time.
The ERP behaved correctly throughout. It was asked to create documents and it created them. The integration is what failed, and it failed the way integrations usually do — not with an error, but by doing exactly what it was told, twice.
Released or unreleased is the first question, and it is not a matter of taste
SAP’s clean-core doctrine states the rule most clearly: extend through released APIs and extension points, or side-by-side on BTP, and do not modify core ABAP. The sanctioned write paths are BAPIs, OData through SAP Gateway, IDocs and Business Events; the unsanctioned ones are direct table updates, unreleased function modules and screen recording over SAP GUI. Every vendor draws the same line under a different name — Business Events in Dynamics 365 F&O, SuiteTalk REST in NetSuite, FBDI loads in Oracle Fusion, XML-RPC in Odoo, the Service Layer in SAP Business One.
Be honest about what the released path costs, because pretending it is free is why teams go around it. It is slower, sometimes missing the field you need, and it will make you model a 5,000-row load as 5,000 calls. A direct table insert is faster to write and faster to run — and it is what an upgrade breaks without telling you, because nobody guaranteed the shape of a table you were not meant to touch. That is not a shortcut with a risk attached; it is a dated liability, and the date is the next major version.
Where no released write path exists — and in Bulgarian accounting packages like Microinvest Delta Pro or Ажур L there often is not — keep the UI automation, but rebuild it as versioned Playwright code with nightly contract tests against the target, on a scoped service account, and record it as debt with a review date. Selectors remain the largest single source of breakage in any bot estate.
At-least-once delivery without an idempotency key is a duplicate-payment machine
Every queue, HTTP client and orchestrator you are likely to use defaults to at-least-once delivery. That is the correct default, and it means the same instruction will sometimes execute twice.
The killer is the ambiguous timeout. A synchronous post that times out — Power Automate cuts outbound synchronous HTTP at 120 seconds — tells you nothing about whether the ERP committed. It may have committed and lost the response on the way back. Retry and you risk a duplicate; do not retry and you may have posted nothing.
The key must derive from business identity, never from the attempt: supplier plus invoice number plus gross amount for an AP posting, customer PO number for a sales order. A UUID generated at retry time is not an idempotency key, and neither is a timestamp.
Most ERPs offer nothing here; there is no Idempotency-Key header waiting for you. The ledger is yours to build: a table you own, keyed on the business key, holding the ERP document number that key produced — written before the call, updated after it. A retry becomes a lookup, and it belongs at the ERP boundary, not the queue: deduplicating messages is not deduplicating postings, because the second attempt usually arrives as a different message, from a different worker, after a restart.
HTTP 201 is a statement about your request, not about the business object
A 2xx response means the request was accepted and usually that a document number came back. It does not mean the object exists in the state you intended: defaults get applied, substitution rules rewrite a cost centre, and a field you sent is dropped silently because it was not part of the released structure.
So the assertion belongs on the read-back. After writing, read the record through the interface the business reads it through — the reporting view, the CDS view, the OData entity the finance report is bound to — and compare a short, named list of fields, not whole payloads, which always differ until everyone stops reading the diff. An integration test that does not round-trip is testing your own serialiser.
The write that succeeds against a deprecated interface is the failure nobody sees
The mechanism is specific. The vendor ships a version, a structure is superseded, and an old endpoint is kept for backward compatibility — where it now writes into a legacy structure nothing downstream reads. Your call returns 200, your orchestrator records success, and the dashboard stays green, because the dashboard reads run status rather than business outcome.
No technical monitor catches this, because technically nothing failed. It surfaces when someone notices at month-end that a total is wrong — by which point there are weeks of writes to reconcile.
Three controls, cheapest first:
- A daily reconciliation count: documents your orchestrator believes it created in a period, against the ERP’s own reporting view for that period, alerting on a drift of one.
- A canary write into the sandbox after every ERP patch and connector upgrade, asserted with a read-back on named fields.
- An interface version pinned explicitly in the call, so a change of server default becomes a deliberate migration.
A status column and a cron job stop working at the first case that waits
The pattern is correct for longer than people admit: a status column, and a job that picks up rows in one state, does the work and moves them on. Do not replace it before you have to.
It stops holding at four points: when a case waits days on something outside your control, such as a goods receipt or an approval; when a committed step must be undone because a later one failed; when different steps need different retry policies rather than one blanket rerun; and when someone has to answer “where is case 4471, and why” without reading your source.
The tell is in the schema. Once the table has acquired retry_count, last_error, locked_by and locked_at, the status enum is past eight values, and somebody has written a second cron whose only job is to unstick rows the first left running after a crash — you have built a workflow engine, without the history, the timers or the visibility.
A durable orchestrator buys failure handling and charges you determinism
Flaviu Cristian’s observation, which Temporal likes to cite, is that failure-handling code is more than two-thirds of a production system. That is the honest reason a 200-line happy path takes three months to make trustworthy, and it is what durable execution sells: an event-sourced history that survives process death and doubles as an audit trail, per-activity retries with backoff, timers measured in days, and compensation for committed steps.
What you pay is determinism. Workflow code cannot call the wall clock, a random generator, the network or a database; all I/O moves into Activities, outside the replay path. And adding, removing or reordering command-generating calls breaks executions already in flight unless you use patching or Worker Versioning — which changes your deployment process, not just your code.
The ceilings arrive in year two. Temporal caps event history at 51,200 events and 50 MB per execution, warning at 10,240 events, so long-running entity workflows must Continue-As-New before they get there; Camunda 8’s guidance is to size for roughly 20x average load. Power Automate allows 500 actions per flow and retains run history for 30 days — read that twice: if the audit trail for a financial posting lives only in run history, it stops existing after a month. n8n prunes at 336 hours and 10,000 executions by default. Write the audit record somewhere durable that you own.
Where this stops applying
If you write a handful of low-value records a day, with no money moving and no statutory document produced, a status column and a cron job is the right architecture, and an orchestrator is on-call burden without a return. The decision point is not volume; it is whether a duplicate or a lost write costs money or breaks a control.
The business case differs here too. Eurostat put Bulgaria’s 2025 hourly labour cost at EUR 12.0 against an EU average of EUR 34.9, so an integration justified purely on eliminating retyping has close to triple the payback a Western template assumes. Justify it on duplicate-payment exposure and cycle time.
Finally, about us. Palamed has four deliverable engagements: a European car marketplace with 300,000+ listings, a platform for an AI automation agency, the Ministry of Education and Science digital dictionary at beron.mon.bg, and email-marketing automation for a beauty brand, where the roughly 60% reduction in outreach time is the client’s own measurement. None is a production ERP write integration. Everything above is the published pattern, the vendors’ documented limits and the doctrine the write path rests on.
Which leaves two questions for any integrator, us included. What is the idempotency key derived from, and what does the read-back assert on? If both answers are vague, the rest of the proposal is decoration.
