02Insurance broking
Market Approach Desk
In short
- What it does
- Compares an n8n version and a Python version of an insurance broker's workflow at one job: never sending an insurer the same request twice.
- Why it matters
- Approaching the same insurer twice for one risk can cost the placement, and an email that reached an underwriter cannot be recalled.
- What I built
- Built the same broker workflow in n8n and in typed Python, a test harness that makes two runs overlap, and a fake insurer that counts what actually arrives.
- Key result
- When two runs overlapped, the n8n version sent the same approach twice and the Python version once. The n8n workflow was run through a simulator of its exported nodes, not a live n8n server.
Overview
An n8n workflow and a typed Python service implement one irreversible insurance workflow; a harness forces two executions of each to overlap, and a fake carrier receiver counts what actually arrived.
- Problem
- A broker approaching the same carrier twice for the same risk blocks the market: the underwriter declines to re-quote and the placement can be lost. An email that reached an underwriter cannot be retracted.
- Built
- A 17-node n8n workflow and a typed Python service for the same approach workflow, a harness that forces two scheduler executions to overlap with a barrier rather than a sleep, a fake carrier receiver, and an operator console with a comparison screen. In the kill test the n8n arm runs through a node-by-node simulator of its exported workflow.
- Hard part
- Measuring at the receiver rather than asking either arm what it did, then showing that SELECT … FOR UPDATE SKIP LOCKED — not only the cleared due_at — stops a second claim when two claim transactions overlap before either commits.
Evidence
approaches the fake carrier counted under forced overlap: n8n arm vs Python arm
The n8n arm runs through a node-by-node simulator of its exported workflow, not a live n8n instance. The duplicate comes from the workflow having no claim boundary across executions, not from n8n itself.
distinct idempotency key in both arms — the same key reached the carrier twice from the n8n arm
Kill test · forced overlap
n8n arm
2
approaches observed at the carrier
read due → filter → send → write back, with no claim boundary between executions; simulated node by node
Python arm
1
approaches observed at the carrier
one transaction claims with SKIP LOCKED and checks placing authority and a 30-day decline cooling period inside it
Two scheduler executions forced to overlap at the moment the failure lives; each arm is counted by a fresh instance of the same fake receiver, with identical fixtures. CI re-runs it on every push to main and on pull requests.
Source: docs/killtest.json, written by the kill test that measured it
Architecture
n8n arm
- A01Input
Schedule trigger
- A02Store
Read due approaches
- A03Code
Filter eligible
outside any transaction
- A04Effect
HTTP send
- A05Store
Write state back
Python arm
- B01Input
Scheduler
- B02Gate
Claim in one transaction
FOR UPDATE SKIP LOCKED + authority and decline-cooling checks
- B03Store
Commit
- B04Effect
Send
outside the claim transaction
- B05Store
Record outcome
Two implementations of one workflow, one harness, one receiver design.
In the kill test the n8n arm runs through a node-by-node simulator of its exported workflow. Each arm sends to a fresh in-process fake receiver, which counts approaches per (placement, market, stage).
- InputArrives from outside the system
- CodeDeterministic code
- GateDecides whether work proceeds
- StoreDurable state
- EffectAn irreversible or outbound effect
Engineering notes
Why the n8n arm duplicates
The workflow is what a competent automation engineer would build. There is no transaction around its steps and no lock across executions, so a second execution reads the due set while the first is between its send and its write. Both see the same approach as eligible, and both send.
That is not an n8n bug. A review corrected the project's first framing: n8n's Postgres node runs free SQL, so a claiming UPDATE … SKIP LOCKED … RETURNING can move the claim into one statement — at which point the reliability boundary lives in the database and the engine is a scheduler around it. That variant is named and explicitly not measured.
What the Python arm does instead
The claim and its conflict rules run in one transaction with SELECT … FOR UPDATE SKIP LOCKED, so a concurrent scheduler skips a claimed row instead of blocking on it. Two rules can refuse an approach: a carrier outside the broker's placing authority, and a carrier that declined this risk within the last 30 days. Both are implemented, though no test exercises either refusal, and nothing on the demo path classifies a reply as declined, so the cooling rule has had nothing to act on yet. A third, already approached, is kept in the code but cannot fire today, because the unique constraint on (placement, market, stage) already allows only one approach. The send is deliberately outside that transaction: holding it open across a network call would quietly turn two workers into one.
Because the claim also clears due_at, the headline test alone cannot tell the lock from the column. A second test forces two claim transactions to overlap before either commits; it was verified by hand to go red with the lock removed.
Where a model is allowed
Only in classifying an underwriter's reply into a closed set of five values, with abstention. The response type carries no identifier, state, permission or numeric field, so a model cannot express 'approach this carrier'. No live model has been called for this project; the shipped classifier is a declared stand-in.
Screens


Limitations
As the project states them. Read these before relying on any number above.
- The n8n arm is measured through a simulator of its exported workflow, not a live n8n run; Retry On Fail, resuming after a Wait node and the HTTP transport are not modelled.
- One failure mode — overlapping scheduled executions — is proven; five others are analysed and labelled as analysed.
- The claim is at most one accepted approach per (placement, market, stage) under the tested contract, not exactly-once delivery.
- A scheduler that dies between claim and send strands that approach: it fails safe (not sent) and is recovered manually.
- The deployed board is seeded directly, so it shows a state rather than the mechanism reaching it; the mechanism is proven by the kill test in CI.
- No live model has been called: the reply classifier is a declared stand-in, and nothing on the deployed path calls it.
Facts and stack
- n8n workflow
- 17 nodes, importable JSON
- Console tests
- 23 Vitest tests
- Business identity
- (placement, market, stage)
Stack
- Python 3.12
- FastAPI
- SQLAlchemy
- Alembic
- PostgreSQL 16
- n8n
- Next.js 15
- TypeScript
- Vitest
- Docker
- Vercel
- Render
- Neon
Skills shown
- n8n
- Workflow orchestration
- Concurrency control
- SKIP LOCKED
- Idempotency
- Forced-interleaving tests