Make agent-callable writes idempotent, or lose data An engineer from Frihet, a company building an agent-native ERP, explains that agent-callable write operations must be idempotent to prevent data loss and duplicate records. The post details how agents retry failed requests, making duplicate writes common, and demonstrates an atomic idempotency-key pattern using Postgres or KV stores to ensure each logical operation executes only once. An agent-native product isn't a chatbot bolted onto a CRUD app — it's an MCP server that lets an agent do things. Read invoices, create expenses, mark a client overdue. The demo is easy. The part that decides whether the thing survives contact with real traffic is the layer nobody screenshots: what happens when a write is sent twice. This is the unglamorous half of agent-native engineering. Every write operation an agent can invoke needs idempotency, safe retries, and typed recoverable errors — not as a nice-to-have, but as the difference between a product and a liability. Here's why, and how to build it. Distributed systems people have known forever that networks give you at-least-once delivery, not exactly-once. A request goes out, the response gets lost on the way back, the caller doesn't know if the write landed, so it retries. With a human clicking a button, this is rare enough to hand-wave. Agents remove the hand-wave. Three things about how agents call tools push duplicate writes from "edge case" to "Tuesday": 429 and 5xx . The Frihet MCP server fri key, and its own client retries with exponential backoff when it hits that ceiling. A retry after a 429 is MCP itself does not save you here. A tool call is just RPC over JSON — the protocol says nothing about whether invoking create invoice twice creates one invoice or two. That contract is entirely yours to define on the server. If you don't define it, you've defined it as "two." For a read tool, a duplicate is free. For a write tool in a fiscal domain — an ERP that prepares VeriFactu records, IVA and IRPF filings — a duplicate is a phantom invoice in someone's tax quarter. The blast radius is exactly why the boring layer matters. The fix is to let the caller assign a stable identity to a logical operation, independent of how many times it's physically delivered. That's an idempotency key: a client-generated token usually a UUID that stays the same across retries of the same intended write, and changes for a genuinely new one. The rule for the client is simple: generate the key once, at the point of intent, and reuse it for every retry of that same intent. If the agent decides to bill a client, it mints one key and carries it through all four delivery attempts. When it decides to bill a different client, it mints a new one. The server's job is to make the key mean something: the first time it sees a key, do the work and remember the result; every subsequent time, return the remembered result without touching the database again. The subtlety that trips people up is atomicity. "Check if the key exists, then insert it" is a race — two concurrent retries both read "not found" and both do the work. The claim has to be a single atomic operation, which in Postgres is an INSERT ... ON CONFLICT DO NOTHING and in most KV stores a compare-and-set. js import { createHash } from "node:crypto"; interface WriteContext { accountId: string; // scope keys per tenant — never globally toolName: string; // e.g. "create invoice" idempotencyKey: string; // client-supplied, stable across retries } async function runOnce