Every answer costs money, so nothing gets overwritten
When each row is a receipt, upserts are a bug. Three rules that make paid data safe.
Most of the data in my system was paid for. Each row of "what an AI assistant answered to this buyer question" is one model call with web search: a few cents, sometimes more. Over a week that's hundreds of rows. Over a month it's a real budget, and a history you can't buy back. Last week's answer can't be fetched again next week.
One evening I found a button that would have quietly thrown some of it away.
The near miss
My console has a Run again button. It reruns an agent with the same inputs. For a weekly job, "the same inputs" included the date. And the job saved answers with an upsert: insert, or update the existing row for the same day.
So "Run again" on an earlier run would ask every question again, pay again, and overwrite the stored answers for that day with the new ones. We'd pay twice and lose the original. I found it while building something else, and fixed it the same day.
Rule 1: check before you pay, not after
Every paid pipeline now looks at what's already stored for that client and day before it makes a single call, and skips anything it already has. That's not deduplication after the fact. Those calls simply never happen.
This sounds obvious. It isn't how most pipelines are written. The usual order is call, then write, and let the database sort out duplicates. When the call is the expensive part, flip it.
Rule 2: inserts only, enforced by the database
Paid rows are written with ON CONFLICT DO NOTHING, never with an updating upsert. If a row for that
day and question exists, the new one is dropped, not merged over it.
For the tables that matter most, the database enforces it too: a trigger rejects any UPDATE
outright.
sqlcreate trigger raw_calls_no_update before update on raw_calls
for each row execute function raise_insert_only();
Code reviews miss things, and triggers don't. A new version of something is a new row with a new version number. The old one stays.
And Run again now drops the old date. A rerun asks today, as a new set of answers beside the old ones.
Rule 3: keep the raw reply, before you parse it
The worst version of losing paid data is quiet: the model call succeeds, the money is spent, and then the parser trips on an unexpected field and the result never lands.
So every paid call now saves the request and the reply exactly as it came back, gzipped, to private storage, before any parsing. Each one gets an index row with tokens, model, and a checksum. If parsing fails, if a later step crashes, if I change my mind about what to extract, the raw answer is still there, and re-reading it costs nothing.
This paid off within a day. When a new model's output got cut off mid-answer, I opened the raw reply and saw exactly why: it had spent most of its budget thinking before it answered. Without the raw copy that would have been a guess.
The pattern, in one picture
Every arrow after "call the model" can fail without losing the money spent before it.
Where else it applies
Once you think of rows as receipts, you see it everywhere:
- Re-counting old answers with a better extractor? Add a new run beside the old one, never replace. The view simply shows the newest count per pass.
- Client documents I send (reports, decks): a new version is a new row; the same file is never stored twice (checked by hash).
- Imports are dry runs by default and skip what's already there. Running one twice adds nothing. I check that every time.
Why this matters more for small teams
A big company can absorb paying twice. A small one can't. And it's not only money: history is the product. A week-over-week chart is only honest if last week's answers are the answers we actually got last week, not a re-run from today wearing last week's date.
So the rule is short: never pay twice, never overwrite, keep the receipt.