Skip to content

Connecting AI Agents Safely to Enterprise ERP/CRM: Idempotent APIs, Outbox Pattern, and Fault-Tolerant Architecture Blueprint

A blueprint for safe enterprise writes by LLM agents: stable business intent, idempotent APIs, transactional outbox/inbox, reconciliation, and compensation. Mermaid, decision matrices, and measured data from 3,600 requests.

· TankDev Engineering

The ERP created the order. The agent does not know that: the connection timed out after the record committed. The model sees an error, while the warehouse sees a real order. If the agent generates another tool call and tries again, the failure is no longer hallucination. One business intention has become two transactions.

Wrong answers matter in RAG. With write-capable agents, incorrect or repeated actions can reserve inventory, alter CRM records, initiate refunds, or create duplicate sales work. This blueprint designs the boundary between a model proposal and a durable enterprise operation. We follow one approved customer order from an API into ERP, with CRM notification as a separate consumer of the same protocol.

The contract is straightforward: a model proposes; trusted services verify identity, authority, current rules, and approval; acceptance uses a stable business intention; local state and outbox commit together; external delivery can repeat without repeating the receiver’s business effect. We do not use “exactly once” without naming the boundaries that support it.

We also ran an actual experiment: 3,600 load-test requests and seven correctness scenarios using a dedicated PostgreSQL database and loopback HTTP endpoints on TankDev’s VPS. Source, raw latency samples, and result JSON are downloadable. It did not run model inference, AWS API Gateway, or a customer ERP; it measures idempotent command acceptance and transaction/inbox behavior.

1. Choose the intervention: prompting, RAG, fine-tuning, or an agentic workflow

A model missing today’s price does not necessarily need a better prompt. If it finds the right product but creates the order twice, fine-tuning does not change that transaction boundary either. The matrix assigns responsibilities rather than declaring mutually exclusive products. These methods can coexist; write workflows need transaction safety regardless of the other techniques.

Decision matrix: method, cost, latency, and setup
MethodWhen?Cost profileLatency profileSetup expectationBoundary it does not solve
Prompt engineeringInstructions, output format, or task definition is insufficientLow setup; per-call token cost remainsNo extra retrieval; longer prompts may still add timeHours to a few daysDoes not supply current enterprise facts, authorization, or write deduplication
RAGAnswers need current, authorized source evidenceIndexing + storage + retrieval + inferenceAdds retrieval/reranking; measure itDays to weeksDoes not guarantee source correctness or atomic writes
Fine-tuningLabeled examples address repeated behavior/format errorsData preparation + training + evaluation + inferenceModel-dependent; not automatically fasterWeeks, longer without usable dataNot a live inventory/pricing authority or transaction deduplicator
Agentic workflowTools, multistep decisions, and enterprise actions are requiredOrchestration + tools + retries + review/operationsMultiple calls and queue time; requires a run budgetWeeks to months, depending on ERP contractsDoes not provide safe writes without policy, idempotency, outbox, and reconciliation

Setup ranges are planning estimates for accessible APIs/data and a small initial scope, not measurements or quotations. Access approval, labeled data, and acceptance testing can change the schedule. Universal millisecond rankings would be misleading without holding models, context, tools, and networking constant. The matrix is also available as copyable Markdown.

Our RAG versus fine-tuning guide explores retrieval and training choices, while TypeScript/LangGraph architecture covers roles and state. Here, the focus is the transaction contract when those workflows write into enterprise systems.

2. Write authority: proposal → policy → command

A model-generated customer_id, total, or approved:true is not authorization. The tool gateway validates structure, derives tenant/service identity from authentication, checks object access, and asks domain services to validate quantities, price versions, discounts, and operation type. It then builds a command tied to business intent, payload hash, and approval. The model has no unrestricted UPDATE or create_payment capability in this design.

json
{
  "operation": "sales-order.create.v1",
  "business_intent_id": "intent-from-trusted-workflow",
  "customer_ref": "authorized-account-reference",
  "lines": [{"sku": "BOOT-42", "quantity": 10}],
  "price_quote_ref": "versioned-expiring-quote-token",
  "approval_ref": "approval-bound-to-canonical-payload"
}

Customer messages, CRM notes, and retrieved documents are untrusted input. Text claiming management approval cannot expand tool permissions. Type safety validates representation, not intent. A valid customer identifier can still refer to the wrong account; evidence binding, business rules, and review address that risk. Separate read and write tools through permissions, networking, and service identities.

3. Idempotency: tool_call_id and business intention are different identities

An agent replan can generate a new tool_call_id, an HTTP retry a new request ID, and relay delivery a new attempt. None necessarily represents another order. Separate business_intent_id for commercial intent, idempotency_key for API replay, command_id for durable acceptance, and event_id for the logical delivery event. Trace IDs are for observation, not write deduplication.

Do not let prompts invent idempotency keys. A trusted workflow persists a stable key for the same initiated or approved work. Scope it to authenticated tenant and operation version. The canonical request hash includes account, business intent, items, quantities, quote versions, and outcome-affecting options, excluding volatile transport timestamps, traces, and attempt counters. Version canonicalization so JSON field order does not change request identity.

API replay contract
ConditionBehaviorInvariant
Same key + same hashReturn original acceptance/command_idNo new order or outbox event
Same key + different hash409 conflictNo silent payload replacement
Different key + same business intentDomain uniqueness plus hash checkAgent replans do not create another order
Explicitly distinct business intentAuthorize and accept new workLegitimate second orders are not incorrectly merged
Revoked access or different accountDeny before returning a cached responseHistorical results do not leak

Using only a product/quantity hash as the key is also wrong: the customer may legitimately order the same item tomorrow. Semantic deduplication of similar text can silently alter business intent. Create that intention explicitly and use the hash to detect mutation. HTTP method idempotency is distinct from your POST endpoint’s application contract; a header name alone creates no guarantee.

APIs such as Stripe provide useful idempotent-request contracts, but response retention and failure-retry semantics can vary by API version. Derive an ERP adapter’s replay safety from that receiver’s documented and tested behavior.

4. PostgreSQL transactions and outbox: make local state and intent commit together

Writing an order and then publishing leaves a crash gap. Publishing first can expose an order whose database transaction later fails. Outbox commits the local order and transmission intent together, as described in AWS transactional outbox guidance. Delivery to a broker or ERP can still repeat.

sql
CREATE TABLE command_receipts (
  id uuid PRIMARY KEY,
  tenant_id uuid NOT NULL,
  operation_version text NOT NULL,
  idempotency_key text NOT NULL,
  customer_ref text NOT NULL,
  request_hash text NOT NULL,
  accepted_response jsonb,
  UNIQUE (tenant_id, operation_version, idempotency_key)
);
CREATE TABLE sales_orders (
  id uuid PRIMARY KEY,
  tenant_id uuid NOT NULL,
  operation_version text NOT NULL,
  business_intent_id uuid NOT NULL,
  request_hash text NOT NULL,
  state text NOT NULL,
  UNIQUE (tenant_id, operation_version, business_intent_id)
);
CREATE TABLE outbox_events (
  event_id uuid PRIMARY KEY,
  aggregate_id uuid NOT NULL REFERENCES sales_orders(id),
  event_type text NOT NULL,
  schema_version integer NOT NULL,
  payload jsonb NOT NULL,
  published_at timestamptz,
  UNIQUE (aggregate_id, event_type, schema_version)
);
sql
-- Authentication, object authorization and approved-payload checks precede this.
BEGIN;
INSERT INTO command_receipts
  (id, tenant_id, operation_version, idempotency_key, customer_ref, request_hash)
VALUES (:command_id, :tenant, :operation, :key, :customer, :hash)
ON CONFLICT DO NOTHING RETURNING id;

-- No inserted row: use a NEW statement at READ COMMITTED to read the
-- committed receipt, recheck access/hash, and return its stored acknowledgement.
-- Conflict hash => rollback and 409. Do not execute the domain write again.

INSERT INTO sales_orders
  (id, tenant_id, operation_version, business_intent_id, request_hash, state)
VALUES (:order_id, :tenant, :operation, :intent, :hash, 'PENDING_ERP')
ON CONFLICT DO NOTHING RETURNING id;

-- New key but existing intent: compare the existing order hash and reuse
-- that order, or reject 409. Insert an event ONLY for a newly created order.
INSERT INTO outbox_events
  (event_id, aggregate_id, event_type, schema_version, payload)
VALUES (:event_id, :order_id, 'sales-order.accepted', 1, :payload);
UPDATE command_receipts SET accepted_response=:response WHERE id=:command_id;
COMMIT;
-- No remote model/ERP call inside this transaction.

This SQL sketches branches; it is not a script that executes every statement unconditionally. The runnable lab separates new receipts, receipt replay, different-key/same-intent, and rollback paths. Unique constraints arbitrate concurrency; an application-level select-then-insert check is insufficient. Evaluate PostgreSQL ON CONFLICT with the chosen isolation level.

Our POST contract returns 202 Accepted with stable command/order identifiers, not proof of ERP application. Retries return that acceptance; an authorized GET supplies current status. Retain idempotency records beyond the supported retry/replay window and manage key reuse after expiry. A longer-lived business-intent record or tombstone can preserve the second deduplication boundary.

5. Copyable architecture: separate the agent from the transaction engine

The diagram is provided as Mermaid source. The write path is an observable service boundary, rather than an opaque tool loop hidden inside a model. Adapt it to orders, CRM activities, or refund requests with appropriate contracts. When reusing it in documentation, retain a link to the TankDev blueprint alongside the explanation.

mermaid
flowchart LR
    A["LLM proposes a business action"] --> G["Tool gateway: schema and authorization"]
    G --> P["Policy, approval, stable intent"]
    P --> D["PostgreSQL transaction"]
    D --> C["Command receipt + order + outbox"]
    C --> R["Relay: bounded retry, lease, fencing"]
    R --> E["ERP receiver: idempotent operation"]
    E --> S["Confirmed external reference"]
    E -->|"ambiguous outcome"| X["Reconciliation or human review"]
    P -->|"not authorized"| H["Reject without business write"]

The relay reads committed outbox records, claims work with a lease, and performs network I/O outside the transaction. Persisted outcomes check ownership tokens. A stale worker cannot overwrite a newer owner’s state; fencing cannot undo its external call. Leases, deadlines, attempt budgets, and oldest-undelivered-event alerts belong to one operating contract.

A broker is not mandatory at the start. A PostgreSQL outbox poller with bounded workers can suit modest volume. Independent consumers, retention/replay, or event volume can justify a broker. Kafka or RabbitMQ does not automatically join an ERP transaction. Our asynchronous LLM architecture article expands worker and rate-limit design.

mermaid
sequenceDiagram
    autonumber
    participant A as Agent
    participant G as Command API
    participant D as Local PostgreSQL
    participant R as Outbox relay
    participant E as ERP receiver
    A->>G: POST, stable key and intent
    G->>D: Commit receipt, order and outbox
    D-->>G: Accepted command ID
    G--xA: Response lost after commit
    A->>G: Retry same key and payload
    G->>D: Read existing receipt, verify hash
    D-->>G: Original acceptance
    G-->>A: 202 with original identifiers
    R->>D: Claim committed outbox event
    R->>E: Deliver stable event ID
    E->>E: Commit inbox and business record
    E--xR: Acknowledgement lost
    R->>E: Redeliver same event ID
    E-->>R: Same external result, no second write

6. Where is the inbox? The boundary in a legacy ERP

A receiver inbox stores event_id and payload hash. Same ID/same hash returns the previous result; same ID/different hash requires conflict handling or quarantine. If inbox insertion and the business write share one local transaction, the receiver can suppress duplicate effects within that boundary. That is exactly how our simulated ERP receiver works.

Writing an adapter inbox before calling a remote ERP API does not provide the same guarantee. If the adapter dies after ERP success, local completion can remain unknown. Reliable options include native ERP idempotency, an ERP-side unique external_reference with atomic creation, or an equivalent vendor-supported contract. Query-before-create can itself race without receiver-side uniqueness or atomicity.

Without those capabilities, use UNKNOWN_EXTERNAL for ambiguous outcomes, then external-reference lookup, reconciliation, and human review where needed. Generating another key and issuing create again does not solve uncertainty. Some operations should remain nonautomatic if the ERP cannot support the required write contract.

Subsequent cancel/update events need aggregate versions too. Older updates must not overwrite newer state; use per-aggregate ordering, expected versions, or conflict review. Deduplicating event IDs does not repair two distinct events applied out of order. CRM delivery can fail independently of ERP delivery, so one completion flag must not hide separate outcomes.

7. Recovery decision matrix: what should be retried?

Failures and recovery decisions on the agent write path
SymptomWhat is known?Safe next stepBehavior to avoid
Schema/domain rejectionWork was not acceptedCorrect input or clarifyUnbounded retries of the same error
401/403Authority is absent/invalidResolve identity/permissionsHave the model bypass policy with another tool
Same key, changed hashContent changed under one identity409; explicitly decide new work/approvalSilently overwrite the receipt
POST timeoutAcceptance may have committedLookup/replay with the same keyCreate another order with a new key
ERP timeoutExternal effect is ambiguousExternal reference and reconciliationBlind create retry
Transient 429/5xxQuota/service issue may be temporaryClassify error; honor Retry-After; bounded backoffTreat every 429 as transient
Changed policy/quote versionOld approval does not cover new contentRevalidate and obtain new approvalTransfer approval to a new price
Partially completed workSome effects already existSaga/compensation or reviewTreat a local rollback as distributed undo

LLM quotas and ERP write limits are different budgets: tokens/requests on one side, accepted operations on the other. Track total duration, tokens, attempts, and spending across the run. Retries multiply when SDK, agent framework, proxy, and worker each retry independently. Assign ownership and inspect the behavior of other layers for their deployed versions.

For example, OpenAI’s rate-limit documentation notes that failed requests can consume limits, valid Retry-After hints should be respected, and application/SDK retries need combined bounds. Billing or usage-cap failures may require action rather than waiting. Other providers need their own error/header contracts.

text
fallback_delay = random(0, min(cap, base * 2^attempt))
delay = max(valid_retry_after, fallback_delay)
retry only if attempt_budget AND job_deadline allow it

For enterprise writes:
  preserve business_intent_id, command key and external reference
  classify UNKNOWN_EXTERNAL before deciding to repeat a side effect
  never restart the full LLM workflow merely to resend a committed event

If payment succeeded but ERP rejected the order, compensation is not deleting a database row. Policy may require releasing a reservation, requesting a void/refund, or operational review. Compensating commands also need stable identity, authority, approval, and idempotency. A shipped item cannot be rolled back in a database sense.

8. Actual TankDev measurements: 3,600 HTTP requests and seven correctness scenarios

The experiment ran on November 5, 2026 in Europe/Istanbul; the raw UTC start is November 4 at 21:16:35. A dedicated database kept production tables untouched. Business data was synthetic. The HTTP client and ASGI server shared one Python process, with PostgreSQL separate. The receiver simulated ERP by committing an inbox and business record in one transaction.

Reproducible environment and workload
ComponentActual setting
Host2 logical CPUs; 8,326,615,040 bytes RAM; shared VPS
Process boundLab process: 1 CPU-equivalent quota, 512 MiB memory limit
RuntimePython 3.12.3; asyncpg 0.31.0; HTTPX 0.28.1; Uvicorn 0.38.0
DatabasePostgreSQL 16.15; fsync=on; synchronous_commit=on; shared_buffers=128MB
NetworkSame-host loopback HTTP; no TLS/proxy/broker
Load50 warm-up; 3 × (1,000 new + 200 replay); offered 200 requests/s; at most 8 in flight
MeasurementNearest-rank p50/p95/p99; client scheduling wait separate
Not coveredLLM/RAG generation, WAN, AWS Gateway, real ERP, production RBAC, process-kill recovery
All three trials: latency in ms, CPU time in seconds
TrialNew p50New p95New p99Replay p95Scheduling p95HTTP/sLab CPU s
12.9954.22313.1042.8691.075200.0512.766954
23.0784.147.2193.3941.071200.0772.791978
32.9313.8844.7242.7471.066200.0442.665606

Pooling the raw samples for 3,000 new requests gives p50 3.003 ms, p95 4.102 ms, and p99 7.629 ms. The 600 replay requests had p95 3.064 ms. Load trials observed no HTTP errors and verified 1,000 orders plus 1,000 outbox records each. These are short bounded-load results, not a production SLO or saturation-capacity claim.

Approximately 200 HTTP/s reflects the offered rate, not a server ceiling or real-ERP capacity. Starting the first request at t=0 can produce a slightly higher short-window quotient. Scheduling waits are reported rather than hidden, and adding two p95 values would not establish an end-to-end p95.

Actual transactions with controlled response-loss tests
ScenarioObserved outcome
8 concurrent requests, same key1 order, identical order_id
Same key, changed quantity409, still 1 order
New key, same business intentionDomain uniqueness preserves 1 order
Injected failure before outbox0 after rollback, 1 after retry
Naive API, delayed response after commit2 HTTP attempts, 2 order records
Idempotent API, same response loss2 HTTP attempts, 1 order record
Receiver commits, acknowledgement times out2 delivery attempts, 1 simulated ERP record

Response loss was induced with a 200 ms post-commit response delay and an actual 50 ms client timeout, followed by resubmission. This is not a cable-cut, failover, or process-kill test. A separate bounded batch of 100 outbox events was delivered and reconciled. The lab receiver’s transactional boundary does not automatically extend to a remote ERP adapter.

The experiment bundle contains source, pinned Python dependencies, environment information, 3,600 raw samples, and result JSON. The result JSON is also separately available. Reproduction means measuring the protocol and distribution on your system, not expecting identical timings.

9. An actual cost input: a USD 100/month VPS and separate consumption budgets

The owner supplied an actual VPS price of USD 100/month. That is a fixed shared-infrastructure input, not a model-token invoice. The experiment made no paid-model calls or AWS API Gateway requests, so those consumption items were zero. We did not separately meter PostgreSQL CPU, disk I/O, or other services, and do not claim a fully measured marginal cost.

Allocating the monthly host bill; volumes are scenarios
Successful logical operations/monthIf all USD 100 is allocated to these operationsEvidence type
10,000USD 0.010000 / operationAssumed monthly volume
100,000USD 0.001000 / operationAssumed monthly volume
1,000,000USD 0.000100 / operationAssumed monthly volume
Experiment: 3,000 logical acceptances8.224538 lab-process CPU secondsMeasured; excludes database CPU
text
C_host_per_success = monthly_host_bill * allocated_share / successful_jobs_month
C_llm = input_tokens / 1,000,000 * input_unit_price
      + output_tokens / 1,000,000 * output_unit_price
C_operation = allocated_host + model_attempts + retrieval + delivery
            + observability + review + expected_error_cost

For a hypothetical 10,000-input-token RAG call:
  input component = 0.01 * input_unit_price
  This is a formula, NOT a measured AWS/LLM bill from this experiment.
Use the actual model, cache usage, region, billing mode and dated invoice/rates.

An acceptance endpoint around four milliseconds does not imply four-millisecond model inference. End-to-end duration includes retrieval, generation, policy, acceptance, queueing, and external application. Measure stage distributions and actual trace totals. Divide business cost by logical operations reaching the specified success state, not HTTP attempts. A pending ERP operation does not yet count as an ERP-completed success.

Idempotency’s value is not merely a cheap HTTP reply. The naive experiment produced two records for one intention; real duplicates can create inventory, fulfillment, communication, and review costs. We do not assign monetary savings without operational evidence. The invariant is first a correctness requirement; its financial value comes from actual business records.

10. Production acceptance: a successful demonstration is one checkpoint

Bind approval to payload hash, quote versions, and expiry. Re-evaluate changes to account, quantity, or price. Check current authorization on every access, including replay endpoints. Correlate commands, outbox events, external operations, and compensations without filling logs with unnecessary customer content, credentials, or raw payment data.

Acceptance should cover pre-commit failure, post-commit response loss, relay crashes, lost receiver acknowledgements, lease takeover, competing workers, stale approvals, out-of-order events, and recovery. Process-kill, failover, actual ERP, and security boundaries not exercised in this lab remain production acceptance work. Seven passing scenarios do not prove safety under every failure.

Operational metrics include duplicate attempts, suppressed effects, hash conflicts, UNKNOWN_EXTERNAL count/age, oldest outbox age, delivery-attempt amplification, and reconciliation discrepancies. A business SLO concerns verified external completion under a declared workload, rather than merely returning 200/202. A write kill switch should preserve visibility and reconciliation for already accepted work.

At TankDev, we design AI development and integration, API/system integration, and custom software around these transaction boundaries. Scope includes models, tool contracts, authority, durable commands, recovery, and measurable acceptance. Our PoC-to-production guide covers the wider production architecture.

A system that avoids duplicate orders does not rely on a model remembering an instruction to act once. It preserves work identity and specifies the effects of replay at each boundary. To examine those contracts in your ERP/CRM, discuss the technical scope with TankDev. Start by establishing how to know whether an operation occurred after its response disappeared.

The full text is also available as Turkish Markdown and English Markdown, preserving the decision matrix, Mermaid source, and SQL. The visible questions below also appear in matching FAQPage JSON-LD. Markup is not a visibility guarantee: Google’s official update log records the removal of FAQ rich results in 2026. Verifiable content and accessible evidence are the substantive reference material.

Frequently Asked Questions

01Can AI hallucination damage an enterprise database?

Uncontrolled write tools can create incorrect or duplicate operations. Schema validation is insufficient: combine tenant/object authorization, business rules, source versions, approval, and idempotent domain operations.

02Can LLM tool_call_id serve as the idempotency key?

Not reliably by itself. Replans can create new call IDs for the same intention. Persist stable business-intent and API identities in a trusted workflow, with domain-level deduplication as well.

03What if the same key carries a different payload?

Authorize access, then compare canonical hashes. Changed content returns 409 rather than overwriting history. Create a new business intention and approval explicitly when new work is intended.

04Should an agent resubmit an order after a POST timeout?

Not as new work. Acceptance may have committed. Retrieve/replay with the same key and content, or query authorized status. Ambiguous ERP outcomes require external-reference reconciliation.

05Does outbox require Kafka?

No. A local order/outbox transaction followed by a bounded poller/worker can be sufficient initially. Consumer independence, replay, retention, volume, and operating requirements determine broker needs.

06Does an adapter inbox guarantee exactly-once in a remote ERP?

No. If inbox and ERP effects do not share an atomic boundary, a crash window remains. Use native idempotency, unique external references, or a supported atomic contract; otherwise reconcile uncertainty.

07How should LLM API rate limits be handled?

Combine provider request/token limits, valid Retry-After hints, global concurrency, and tenant budgets. Prevent nested SDK/application retries. Billing or persistent quota failures may require corrective action.

08Can RAG or fine-tuning prevent duplicate orders?

No. They can improve retrieval or behavior without replacing transactions, idempotency, business-intent uniqueness, or receiver deduplication.

09Can an old approval authorize a new price?

Approval must not silently transfer to changed content. Check quote/policy versions, payload hash, account, and expiry, and request new approval when required.

10Is the 4.102 ms figure RAG or AWS latency?

No. It is pooled p95 for 3,000 new requests to a same-host idempotent HTTP acceptance endpoint. The experiment excludes LLMs, RAG retrieval, TLS, AWS Gateway, and actual ERP.

11What is the operation cost on a USD 100/month server?

Allocate host cost as USD 100 × assigned share / successful logical operations per month. Volume is an assumption, with model, data, delivery, observation, and review added separately. This is not a fully measured total cost.

12Can TankDev implement this blueprint for an existing ERP/CRM?

Where supported APIs, access, and transaction contracts permit it, we can design and develop the integration. Start with ownership, authorization, external identities, idempotency, and recovery from ambiguous outcomes to define automation limits.

Related notes

WhatsAppDirect contact