Idempotent Write-Backs: How a Clinical Dental AI Agent Avoids Duplicate Appointments and Double Ledger Entries When a Request Retries

Have you traced what happens when your agent's appointment write reaches the practice management system, commits, and then loses its response to a 29-second API Gateway timeout? If you run a clinical dental agent in production, you have probably already met the result: two appointments in the same operatory, or the same crown charged twice on one patient ledger.
The read path gets most of the engineering attention, and for understandable reasons. Retrieval, evals, and latency are measurable, and a bad read can simply be run again.
A write is different because it changes the system of record. Accordingly, this post covers the write path: how idempotency keys are minted, what the retry policy may and may not do, and how reconciliation closes the cases that neither one can settle.
An idempotent write-back produces the same PMS state whether it runs once or five times. The orchestrator assigns each intended write one key, records the outcome against it, and returns the stored result on every retry.
Why A Duplicate Write Is A Compliance Problem Before It Is An Engineering Problem
The HIPAA Security Rule's integrity standard at 45 CFR 164.312(c)(1) requires covered entities and their business associates to protect electronic PHI from improper alteration or destruction. An agent that posts the same procedure twice has altered a patient's record in a way that nobody authorized, and that is the framing an auditor will bring to it.
What's more, the audit controls standard at 164.312(b) expects you to show which request produced which record. If two ledger entries exist and your logs show one tool call, the gap between them is the finding.
This is why the deduplication store sits inside the same boundary as the rest of the clinical stack. It holds patient identifiers and appointment details, so it belongs under the BAA, in DynamoDB encrypted with a customer-managed KMS key, with CloudTrail data events turned on, as described in our overview of HIPAA requirements for clinical dental AI.
The operational cost follows the compliance cost. A duplicate appointment blocks chair time that the schedule cannot sell twice, and a duplicate charge flows into statements, claims, and the receivables figures behind your dental AR days.
Why Does A PMS Write Retry In The First Place?
Retries are a property of the infrastructure, and no amount of prompt work removes them. The sources of a repeated write in a clinical agent stack include but are not limited to:
- Ambiguous timeouts. The request reaches the PMS and commits, but the response never returns because a Lambda function or an API Gateway integration hit its limit first. The caller sees an error and the record exists anyway.
- At-least-once delivery. SQS standard queues can deliver the same message more than once, and asynchronous Lambda invocations retry twice by default after a function error. Each redelivery is a second attempt at the same write.
- Throttling. A PMS API that returns a 429 during the morning scheduling rush invites a retry, which is correct. The difficulty is that the same retry code usually handles the timeouts as well.
- Model re-planning. When a tool call returns an error, Claude or any other model will often issue the call again, sometimes with slightly different arguments. From the PMS side, that is a brand-new request.
- Human repetition. A caller says "yes, book it" twice because the line was quiet for two seconds. Alternatively, the same patient confirms on the phone and then again through web chat.
All of these produce the same symptom, which is a second write carrying the same business intent. That said, they do not share a remedy, and treating them as one failure class is how duplicates get into the schedule.
The arithmetic is worth doing for your own volume. A group with 12 locations and 400 agent-initiated writes a day, at an ambiguous-timeout rate of 0.5%, faces two unknown-outcome writes every day and roughly 60 every month.
Why A Practice Management System Is Harder Than A Payments API
Payment APIs such as Stripe accept an Idempotency-Key header, store the first response, and replay it for any repeat of the same key. The deduplication is the vendor's problem, and it is solved at the point of the write.
Practice management systems were built for a front desk coordinator who clicks once and looks at the screen. Most of them accept no idempotency header, and their schedules permit double-booking by design, because practices overlap hygiene checks and emergency visits on purpose.
As a result, the PMS will not reject your second appointment as a conflict. It will accept it, since two appointments in one operatory at 2:00 PM is a legal state.
The integration topology adds another hop. Whether you write through the Open Dental API or through a bridge service in front of an on-premises server during an Eaglesoft migration, each relay is one more place where a response can be lost after the commit.
Keep in mind that vendor behavior changes between versions, so the current API documentation for your PMS is the authority on what it deduplicates. Until it says otherwise, the safe assumption is that it deduplicates nothing.
Most practice management systems accept no idempotency header and allow double-booking by design. As a result, deduplication has to live in the orchestration layer, in a store the agent controls.
How Should The Idempotency Key Be Minted?
The orchestrator mints the key, and the model never does. A key the model generates changes whenever the model re-plans, which defeats its purpose on exactly the retries that matter.
The key is a SHA-256 hash of the fields that define the business action. For an appointment, those fields are the practice ID, patient ID, operation type, operatory, and start time; for a ledger entry, they are the practice ID, patient ID, CDT code, tooth number, date of service, and amount.
Normalization comes before hashing, because two renderings of one intent must produce one key. Start times are converted to UTC and snapped to the schedule's slot increment, amounts become integer cents, and tooth numbers are converted to a single system, since tooth 30 in Universal and tooth 46 in FDI are the same molar, as covered in our guide to tooth numbering standards.
Some fields have to stay out of the hash. Request timestamps, trace IDs, and free-text notes written by the model differ on every attempt, and including any of them turns every retry into a new key.
In addition, each attempt carries its own attempt ID alongside the intent key. The intent key answers whether this write has happened, and the attempt ID lets you trace which of three tries actually reached the PMS.
The orchestrator mints the key, never the model. It hashes normalized intent fields: practice, patient, operation type, operatory and start time, or procedure code, tooth, date of service, and amount.
The Reserve, Execute, Record Sequence
A key only helps if it is checked before the PMS call and updated after it. The sequence that enforces this has three steps:
- Reserve. The worker writes the key to DynamoDB with a conditional expression of attribute_not_exists, a status of PENDING, the attempt ID, and a lease expiry. If the condition fails, another attempt already owns this intent.
- Execute. The worker calls the PMS. Where the record exposes a free-text note field, it also writes a short correlation token there, which makes later matching exact.
- Record. The worker updates the key to COMPLETED and stores the PMS record ID and a hash of the response. This stored result is what every later retry receives.
The behavior on a retry depends entirely on what the reservation finds. A COMPLETED key returns the stored record ID without a PMS call, and a PENDING key with a live lease tells the retry to back off.
The interesting case is a PENDING key whose lease has expired. That means a worker reserved the key, may or may not have reached the PMS, and died before recording, so the write goes to reconciliation and is never simply re-executed.
We model the whole thing as a state machine with five states: PENDING, COMPLETED, FAILED_RETRYABLE, FAILED_TERMINAL, and NEEDS_REVIEW. Step Functions Standard workflows suit it well, because the execution history is durable and doubles as part of the audit trail.
The worker reserves the key with a conditional write, executes the PMS call, then records the returned record ID. A retry that finds a completed key returns the stored result and never touches the PMS again.
What Should The Retry Policy Do With Each Failure?
A single retry policy applied to every error is the root of most duplicate writes. The table below separates the failures by what the agent actually knows at the moment of the error.
| Failure | What The Agent Knows | Safe Action |
|---|---|---|
| 429 throttle | The write was not accepted | Retry with exponential backoff and jitter under the same key |
| Connection refused or DNS failure | The request never arrived | Retry under the same key |
| 400 or 422 validation error | The write was rejected and will be rejected again | Mark the key terminal and return the error to the model |
| Timeout, 502, or 504 after send | The outcome is unknown | Reconcile by reading the PMS before any second write |
| Duplicate SQS delivery | The key is already COMPLETED | Return the stored result with no PMS call |
| Slot taken by another patient | A different intent holds the slot | Treat as a scheduling conflict and offer the next opening |
The last row deserves a note, because slot contention between two patients is a concurrency problem and idempotency does not solve it. It needs its own check, typically a fresh availability read immediately before the write.
Retries should also be bounded. Three attempts with backoff, followed by a move to a dead-letter queue, keeps a failing PMS from being hammered by its own client during an outage.
Of course, a caller on the phone cannot wait through three backoff intervals. For AI phone agents, the conversation should acknowledge the request and let the write resolve asynchronously, which keeps the voice path inside the budgets discussed in our post on chairside AI latency.
How Does Reconciliation Settle An Unknown Outcome?
When the outcome is unknown, the only source of truth is the PMS itself. Reconciliation is therefore a read-before-write: the worker queries for a record that matches the intent before it considers sending anything again.
For an appointment, the natural key is the patient, date, operatory, and start time. For a ledger entry, it is the patient, date of service, CDT code, tooth number, and amount.
The correlation token written during the execute step makes this far more reliable. A natural-key match says a similar record exists, whereas a token match says this exact attempt created it.
Three results are possible, and each has one response. If a matching record is found, the worker adopts its ID and marks the key COMPLETED; if nothing is found, the key is released for one more attempt.
If the result is ambiguous, such as two candidate records or a match on every field except the amount, the key moves to NEEDS_REVIEW. A front desk coordinator then resolves it from a queue, with both records and the original request shown side by side.
Be aware that absence is harder to prove than presence. If your integration reads from a synced replica or through a bridge that polls on an interval, the worker has to wait out that interval before concluding that the write never landed.
Finally, a nightly sweep compares every COMPLETED key against the PMS. It looks for ghosts, which are keys with no surviving record, and orphans, which are records carrying an agent token that no key accounts for.
A timeout after the request was sent is an unknown outcome, and resending is unsafe. The agent reads the PMS for a matching record first, adopts it if found, and writes again only when absence is confirmed.
Why Ledger Entries Need Stricter Rules Than Appointments
A duplicate appointment is visible on the schedule and cheap to remove. A duplicate ledger entry is quieter and travels further, into the patient statement, the insurance claim, and the month-end production report.
For example, a D2740 crown charged twice turns a $1,200 patient portion into a $2,400 balance. The patient finds out when the statement arrives, which is the worst possible moment for the practice to find out as well.
The correction also differs. An appointment can be deleted, but a ledger should be corrected with a compensating transaction: an offsetting adjustment that references the original entry and leaves both in the audit trail.
This is why we do not let the agent reverse a ledger entry on its own. A suspected duplicate is routed to review, and a staff member with the right PMS permissions approves the adjustment.
Payments add a second system. When a card charge and a ledger posting belong to one intent, the same key should be passed to the processor's idempotency header and to your own store, so that the two writes cannot drift apart.
If the charge succeeds and the posting fails, reconciliation moves forward and completes the posting. Refunding a patient because your own ledger write timed out converts an infrastructure fault into a billing dispute.
The agent should never delete or auto-reverse a ledger entry. A suspected duplicate goes to a review queue, and the correction is an offsetting adjustment that references the original entry.
How Do You Know The Write Path Is Working?
A write path that has never been made to fail has not been tested. The validation suite for writes is built on fault injection, and it sits alongside the read-side checks in our post on clinical AI evals.
Four injected faults cover most of the ground. The suite kills the worker after the PMS commits and before the record step, delivers every SQS message twice, delays the PMS response past the timeout, and replays a tool call with reordered arguments.
The assertion is the same for all four: exactly one PMS record per intent key. Any run that produces two is a release blocker, regardless of how the model scored elsewhere.
Shadow mode comes next. For a period before go-live, the agent logs the writes it would have made without executing them, and the team compares that log against what the front desk actually entered.
In production, five numbers tell you the state of the path. These are the duplicate rate, the ambiguous-outcome rate, the share of unknown outcomes that reconciliation resolves without a human, the P95 age of the NEEDS_REVIEW queue, and the dead-letter queue depth.
Key retention is the last decision. Appointment keys should outlive the appointment date by several days, and ledger keys should follow the same schedule as the rest of the audit trail, which we cover in our post on AI log retention.
The write path is working when fault injection cannot produce a second record. The suite kills the worker after the PMS commits, delivers every message twice, and asserts exactly one record per key.
Frequently Asked Questions
Should the language model generate the idempotency key?
No. The model can re-plan after an error and emit slightly different arguments, which would produce a new key. The orchestrator derives the key from normalized intent fields such as patient, operatory, and start time.
What happens when a PMS write times out and the outcome is unknown?
The agent does not resend. It reads the PMS for a record matching the patient, date, operatory, and start time, adopts that record ID if one exists, and only issues a second write once the absence is confirmed.
How long should idempotency keys be retained?
Appointment keys should live until the appointment date has passed plus a buffer of several days. Ledger keys should follow the same retention schedule as the audit trail, since they prove which request produced which entry.
Can the agent reverse a duplicate ledger entry on its own?
It should not. Ledger corrections belong in a review queue, where a staff member approves an offsetting adjustment that references the original entry. Deleting the duplicate removes the audit trail the correction depends on.
Does an SQS FIFO queue remove the need for idempotency keys?
No. FIFO deduplication covers a five-minute window on the queue hop only. A timeout between the worker and the PMS, or a redelivery after that window, still needs a key checked at the point of the write.
Scoping The Write Path Before Go-Live
Reads decide whether a clinical agent is useful, and writes decide whether a practice can trust it with the schedule and the ledger. Idempotency keys, a retry policy that distinguishes rejected from unknown, and a reconciliation loop are what turn the second into an engineering property that you can test.
If you're moving a dental agent from read-only to write-back and want a second set of eyes on the design, the team at NexV builds and operates write paths against practice management systems as part of our dental AI software work. Reach out for a working session — we'll map every write your agent makes, classify each failure as retryable, terminal, or unknown, and leave you with a fault-injection plan you can run before go-live.