All projects

Case study · Cabify Spain · Jan to Sep 2026 · flagship project

AQM · Asset Quality Management

Partner fleets wrap their cars in brand or advertising vinyl, and drivers earn a monthly bonus for keeping it in good shape. Checking that used to mean a person looking at photos one by one in a ticketing tool. AQM does it now: capture forms, an AI audit in n8n, one fleet database and a dashboard that operations and ads teams use every day.

23,621 photos auditedn8n automation
n8nn8nClaudeClaudeAWS BedrockAWS BedrockGoogle Apps ScriptGoogle Apps ScriptGoogle SheetsGoogle SheetsGoogle DriveGoogle DriveGitLabGitLabSlackSlack
My roleTechnical owner: forms, workflows, prompts, database, dashboard and documentation. A colleague owned the process.
WhenJan to Sep 2026 · business as usual since 3 Sep
Built withn8n, Claude (vision) through LiteLLM and Bedrock, Apps Script, Sheets, Drive, GitLab CI, Slack
StatusIn production. Handed to Product as the basis for a native version.
23,621
vehicle photos audited by the AI pipeline
97%
accuracy against human review; most of the 3% are unusable photos
30–60 min
from a driver taking the photo to the verdict in the database
12
n8n workflows, documented so the team runs it without me

The problem

Photos arrived through a generic form and someone reviewed them by hand in the ticketing tool. There was no single record of which car carried which vinyl, in what state, or when it was last checked. The monthly bonus depended on those manual checks, and the ads team ran a separate process for campaign wraps.

The fleet also changes every day: cars change drivers, go inactive or move between partners. Any solution had to follow the car itself, whoever uploaded the photo.

What I built

CAPTUREINTAKEAUDITSTOREUSEFour web formsdrivers and threesuppliers, one codebaseIntake webhookphotos to Drive, row toa data table in secondsLive plate OCRwarns while the user ison the formAI auditevery 10 min, onevehicle at a timePlate check4 cases, Slack whenunclearFleet databaseone row per plate, 53columnsFleet export +partner ERPdaily export, ERP every10 minLive dashboardApps Script, 10 tabsMonthly bonusno photo, no payment
The whole circuit. Photos come in on the left; the dashboard and the bonus read from the same database on the right.

1. Capture

Four web forms (drivers and three wrapping suppliers) share one HTML and JavaScript codebase, deployed with GitLab CI to the company's static hosting. The subdomain decides which supplier configuration loads. Each photo slot opens the camera with a silhouette of the car, and the browser checks size, brightness and sharpness before uploading: short side of at least 600 px, brightness between 45 and 236 and a Laplacian variance of at least 40. Uploads run in the background, so the user gets an answer straight away.

capture formsynthetic data
Vinyl registration form, plate and first photo slots
Driver form: the plate and one slot per photo, nothing else. Every extra field lowered completion.
camera guidesynthetic data
Camera view with a car silhouette guide
Camera guide with the car silhouette and a prompt to turn the phone.
plate checksynthetic data
Plate field with a live suggestion
The plate is checked against the active fleet while typing.

2. Intake

A webhook stores each photo in its own Drive folder and writes the submission to an n8n data table at once. A second webhook reads the plate from the photo while the user is still on the form and warns if it does not match what was typed. It never blocks a submission: some damaged cars cannot be moved to show the plate.

3. AI audit

Every 10 minutes a 74-node workflow takes the pending vehicles one at a time. Each photo (both sides, front, rear, headrest and the regional badge) has its own branch: download, resize to 1024 px, and ask Claude for a fixed 9-field JSON: vinyl type, external brand, side, defect, rear label, regional VTC badge, damage, damage detail and whether the damage sits inside the vinyl area. Side photos also go through the collage audit below. Designs with no reference, such as media campaigns, get a stricter subjective audit. Eight branches merge into one record per vehicle, with tokens and cost logged.

Side photoresized to 1024 pxFind the reference12 designs × 2 sidesBuild a collagereal car next to thereferenceVision LLMfixed 9-field JSONMerge 8 branchesone record per vehicle,cost logged
The model never judges a side photo on its own. It gets the real car next to the reference design for that vinyl and side, and answers a fixed schema.
n8n · vehicle photo analysis · 74 nodes
n8n workflow diagram of the AQM photo analysis
The analysis workflow as it runs in n8n, relabelled in English. Open it full size: every photo type has its own branch, and side photos go through the reference collage.

4. Plate check

The model reads the front and rear plates and a small rule set decides between four cases: confirmed; corrected, when front and rear agree against what was typed; conflict, which sends the image and the typed text to Slack with four options and waits up to two days; and unreadable, which keeps what the user typed.

5. One fleet database

Results sync into a single table with one row per plate and 53 columns, enriched with the daily fleet export and a partner's ERP every 10 minutes. The ads campaign is assigned automatically from the ads team's own sheet, so nobody types it twice. An audit expires after three months, and the bonus tab applies one rule: no photo, no payment.

6. The dashboard

An Apps Script web app reads three sources live on every load, with cross filters and CSV export. It replaced a Looker Studio report that needed manual upkeep. When the source moves to the warehouse, only the read function changes.

AQM dashboard · summarysynthetic data
AQM dashboard summary tab
Summary: coverage, vinyl state and audit freshness for the whole fleet.
AQM dashboard · vinyl statesynthetic data
AQM tab with vinyl state by region
Vinyl state by region and fleet, the view operations leads asked for.
AQM dashboard · pipelinesynthetic data
Pipeline tab with cars pending audit or wrap
Pipeline: which active cars still need an audit or a wrap.
AQM dashboard · adssynthetic data
Ads campaigns tab
Campaigns: cars wrapped for each advertising campaign, matched automatically.
AQM dashboard · bonussynthetic data
Bonus payment tab
Bonus: who qualifies this month and why the rest do not.
AQM dashboard · appointmentssynthetic data
Workshop appointments tab
Appointments: cars booked at a wrapping workshop that still show no vinyl.
AQM dashboard · datasynthetic data
Data dictionary tab
Data tab: every filter, column and state explained in the tool itself.
AQM dashboard · fleetsynthetic data
Fleet tab with coverage by audience, category and tariff
Fleet: vinyl coverage by audience, category and tariff.
AQM dashboard · regionssynthetic data
By region tab with a coverage table per region
By region: coverage and vinyl state per region, with the best and worst covered.
AQM dashboard · auditssynthetic data
Audits tab with form responses per week and AI cost
Audits: form responses per week, vehicles audited and the AI cost of the analysis.

How it got here

This is the third version.

  • Jan to Feb 2026, my own models. A labelled dataset, a CNN trained from scratch, YOLOv8 and EasyOCR for plates and a SIFT homography to measure coverage. It ran in batch but confused similar designs. The story continues in the hybrid vision engine.
  • Apr 2026, n8n and a vision LLM. Five parallel analyses per image on Claude through Bedrock. Much richer output, and the version I presented at the global Customer Operations all-hands.
  • Jun 2026, the spreadsheet broke. The main sheet started returning 503 errors under recalculation load. I put an n8n data table in front as a buffer, recovered about 140 skipped responses and moved the design toward one clean, read-only table.
  • Aug to Sep 2026, one circuit. Forms on company hosting, nine fields instead of six, the reference collage, the plate logic, the bonus and the dashboard. From 18 model calls per vehicle in the first n8n version down to about 10.

On 16 September a partner asked in the morning to add the new Madrid VTC badge (the sign private-hire cars must display) to the audit. It was live the same afternoon: a conditional photo in the form, a round camera guide, two new values in the model's taxonomy and no database migration. Barcelona followed the next day with a different badge shape.

Results

  • 23,621 photos audited so far, each with a stored verdict per field.
  • 97% accuracy against human review. Most of the remaining 3% are photos that are too dark, blurred or cropped for anyone to judge, which is why the form now checks quality before upload.
  • 30 to 60 minutes from photo to database, against days of manual review before, and about 1.5 hours a week of supervision.
  • Closed as a project on 3 September: the local teams run it as business as usual, and it went to Product as the basis for a native version.

What I would do differently

  • Build a labelled evaluation set on day one instead of tuning prompts against examples I knew.
  • Get off spreadsheets as the database earlier. The June outage was predictable.

Client, supplier and people names, business figures and internal hosts are left out on purpose. Dashboard and form screenshots are the real tools rendered with synthetic data.