Case study · Cabify Spain · Apr to Sep 2026
COPs Control
Every Tuesday the COPs Control review goes through the deviations of Spain's operational processes. Each owner used to open their own source, copy the numbers, and someone pasted them into a document the day before. The data arrived late, degraded on the way and had no history. I inverted the flow: the data extracts itself, keeps its history and goes to find the person.
The daily run and the Monday alert are two scheduled tasks on the team's Mac mini, the same machine as my local AI agent.
How it works
- 05:00. An Apps Script bridge reads AQM and ADP from their dashboards and a supplier sheet, and leaves three JSON files in a Drive folder that the Mac mini also sees.
- 06:00. The daily run extracts Tableau, reads the bridge files, stores every reading in PostgreSQL with its extraction date, computes range, forecast and state, and rebuilds the web data. If one source fails, only its KPIs are marked; the rest of the run continues.
- Every visit. The team web app, served by Apps Script, reads the latest data file. A link with the owner's name opens only their processes.
- Monday 12:00. One Slack message per owner with their level-1 KPIs off track or below target, at most eight, worst first.
How each KPI is judged
First the realistic range: what is normal for that KPI according to its recent weeks, the median plus or minus 1.5 times its typical spread. August and Christmas are excluded, and a KPI needs at least four clean weeks before it is judged at all. Then the week's value is compared with that range and with the owner's target, if there is one.
| State | When | What to do |
|---|---|---|
| Off Track | Outside the realistic range on the side that hurts | Discuss it: something is happening that history does not explain |
| Average | Inside the range but short of the owner's target | Normal for us, but below where we want to be |
| On Track | Inside the range and meeting the target, or no target | Nothing to look at |
| High | Outside the range on the good side | Understand what went well, in case it can be repeated |
| Not evaluated | Fewer than 4 clean weeks of history, or an atypical week | Nothing can be said yet; calling it green would be a lie |
The bad side depends on the KPI: for a completion rate it is going down, for a cancellation rate it is going up, and the change is coloured by what it means, not by its direction. States are computed for every week of the series, so going back in time shows the state the KPI had then.
What a target does
The range is computed by the system ("if nothing changes, this is normal"); the target is set by a person ("this is where we want to be"). Owners set their own targets in the web app, with save and delete next to the field, and they apply in about ten minutes. A target makes the Average state possible, is drawn on the chart as a dotted line and lets a new KPI be evaluated before it has four weeks of history. Only the KPI's owner and two admins can set one; the web app checks it on save and the Mac mini checks again when applying it.







The Monday message links straight to the board, so it has to work on a phone too: the filters stack into one column and each KPI keeps its chart, its range and the reason for its state.
● Waiting time at pickup · 6.8 min · off track, above the realistic range for 2 weeks
● Completion rate · 91.2% · inside the range but below your target of 93%
● 14 more KPIs with nothing to look at
Open your processes:
…/exec?resp=hubsVersion 1: reading screenshots
With no API access at first, I built an n8n workflow that signed in to Tableau, downloaded each view as an image, asked a vision model to return the numbers as strict JSON and wrote them to a weekly sheet, with tiered Slack alerts. It covered 18 processes and premiered in the July review. It worked, but image extraction occasionally invented a digit, and there was no cheap way to catch it before the meeting. That was the reason for version 2.
Why it is on hold
The bottleneck is Tableau. Each workbook serves a different picture and almost none serves the complete KPI. I tested every technical path:
| Path | Result | Why |
|---|---|---|
| REST API, view data | partial | Works, but returns only what the sheet shows: usually the last week, sometimes one row |
| URL filters on the view | partial | Only on categorical filters; on a relative date they return an empty file |
| Temporary copy of the workbook with weeks as columns | partial | Brought 12 weeks of history for two workbooks, then deleted. Only works where the workbook can be downloaded |
| Metadata API | works | Finds sheets and owners; does not read data |
| Querying the data source directly | blocked | Access denied on every data source tried |
| Reusing the browser session | blocked | Rejects the API tokens |
| Subscriptions, connectors, AI on screenshots | blocked | Tried by the team: does not scale and cannot guarantee complete data |
With warehouse access
- All 56 active processes of the census on one board, with full history from day one instead of in ten weeks.
- One KPI, one versioned SQL query in the repository, so nobody argues about where a number comes from.
- Alerts out of the test loop, with ranges calibrated on months of history rather than seven points.
What I learned
- A census before a dashboard. Listing all 76 processes surfaced some the catalogue called active that no source served; they became decisions for their owners instead of silent gaps.
- Knowing when to stop is part of the work. Writing down why each path failed made the case for warehouse access in one page.
Screenshots show the real board rendered with synthetic values, generic process names and roles instead of people.