Playbook — n8n “atrax fleet sync” failing with 409 / 23505 on p24_l_cars

Workflow: atrax, kravag-scheduled-fleet-updates (id CCx9UMdphmGficDX, bms-4 n8n, 3-min schedule) Alert: N8nWorkflowPersistentlyFailing · First seen: #1174 (2026-06-23)

Trigger

The workflow’s update-atrax-pojazdy-w car-atrax node (a POST to Supabase p24_l_cars_atrax?on_conflict=atrax_id) fails every run with NodeApiError / HTTP 409. The Postgres error is 23505: duplicate key value violates unique constraint "p24_l_cars_atrax_car_id_unique".

Note: despite the name, this node writes to Supabase, not the Atrax API. The 409 is PostgREST surfacing a Postgres unique-violation, not an Atrax rejection.

Confirmation commands

Pull the exact failing node + error from the n8n execution store on bms-4:

docker exec bms-4-n8n-postgres-1 psql -U n8n -d n8n -t -A -c "
SELECT ed.data FROM execution_data ed
JOIN execution_entity ee ON ee.id=ed.\"executionId\"
WHERE ee.\"workflowId\"='CCx9UMdphmGficDX' AND ee.status='error'
ORDER BY ee.id DESC LIMIT 1;" | grep -o '23505[^}]*}'

Find which fleet rows hold a stale atrax_car_id (collision-capable = has_target_row=true):

SELECT c.plates AS fleet_plate, c.atrax_car_id, a.registration_no AS atrax_plate,
       EXISTS(SELECT 1 FROM p24_l_cars c2 WHERE c2.plates=a.registration_no) AS has_target_row
FROM p24_l_cars c JOIN p24_l_cars_atrax a ON a.atrax_id=c.atrax_car_id
WHERE c.plates IS DISTINCT FROM a.registration_no
ORDER BY has_target_row DESC;

Root cause

A vehicle is re-plated in Atrax (e.g. HVLET442 → BET824). The merge_atrax_to_fleet() trigger on p24_l_cars_atrax tries to move the vehicle’s atrax_id onto the fleet row matching the new plate, but the atrax_id is still attached to the old plate’s row. p24_l_cars.atrax_car_id is UNIQUE, so the reassignment raises 23505 → 409 → the whole batch upsert aborts.

Fix (durable — already shipped)

Migration supabase/migrations/20260623_fix_1174_merge_atrax_replate_safe.sql makes merge_atrax_to_fleet() release a stale atrax_car_id from any other row before reassigning, guarded by EXISTS(fleet row for current plate) so cosmetic-only mismatches (*BET561, B ET 633) are never unmapped. Once this trigger is in place, re-plate cases self-heal on the next scheduled run.

If the trigger is somehow reverted, a one-shot manual unblock:

-- release the stale mapping, then re-fire the trigger
UPDATE p24_l_cars SET atrax_car_id=NULL, gps_atrax_installed=false
WHERE atrax_car_id='<offending_id>' AND plates <> '<current_atrax_plate>';
UPDATE p24_l_cars_atrax SET updated_at=NOW() WHERE atrax_id='<offending_id>';

Verify

Wait one schedule cycle (≤3 min) and confirm a success execution:

KEY=$(grep '^BMS4_N8N_API_KEY=' /opt/p24-infra/bms-4/.env | cut -d= -f2- | tr -d '\r')
curl -s -H "X-N8N-API-KEY: $KEY" \
  "http://localhost:5678/api/v1/executions?workflowId=CCx9UMdphmGficDX&limit=1" \
  | python3 -c "import sys,json;e=json.load(sys.stdin)['data'][0];print(e['status'],e['startedAt'])"

Escalation

If 23505 persists with has_target_row=true rows that are NOT a clean re-plate (e.g. two distinct physical vehicles claiming the same Atrax id), it is a fleet data-modeling issue — escalate to the fleet/workflow owner; do not auto-merge rows.