Skip to content
Tahir Aslanli

02Insurance broking

Market Approach Desk

Complete · frozenDeployed / live

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

2 vs 1

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.

1

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

  1. A01Input

    Schedule trigger

  2. A02Store

    Read due approaches

  3. A03Code

    Filter eligible

    outside any transaction

  4. A04Effect

    HTTP send

  5. A05Store

    Write state back

Python arm

  1. B01Input

    Scheduler

  2. B02Gate

    Claim in one transaction

    FOR UPDATE SKIP LOCKED + authority and decline-cooling checks

  3. B03Store

    Commit

  4. B04Effect

    Send

    outside the claim transaction

  5. 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

Placement board listing approaches by carrier with their state and seeded attempt count
The placement board, captured from the live deployment.Captured from the live deployment
Kill-test panel of the comparison screen: the n8n arm observed two approaches with one duplicate, the Python arm one approach and no duplicate
The comparison screen's kill-test panel, rendered from the committed result of 2026-09-11: two approaches reached the fake carrier from the n8n arm, one from the Python arm. The Python arm's one-line summary on it refers to the two conflict rules described in the notes above.Captured from the live deployment

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