Personal project · Visa appointments
A family member needed a Schengen visa appointment at the Greek Consulate General in Los Angeles before a fixed travel date. The booking calendar was permanently full. So we built a watcher to catch the moment a cancellation appeared — and after three thousand clean checks it had found nothing, which turned out to be the single most useful thing it could have told us.
A hard travel date, and an appointment that had to happen before it. The consulate’s online booking calendar showed a run of published appointment days — every one of them fully booked. The received wisdom, repeated on every forum, was that seats do free up: people cancel, and if you happen to be looking at the page in that window, you can take the slot.
Which makes the strategy obvious and miserable. Refresh the page. Keep refreshing. For six weeks. That is not a plan, it is a hostage situation — and it fails precisely because nobody can sustain it at 2am, which is exactly when a cancellation from another timezone might land.
A watcher, running on infrastructure we already had. No API, no documentation, no developer portal — just a public booking page. Reading the site’s own front-end JavaScript showed how its calendar actually worked: a single request returns a fragment of HTML, and each bookable day carries an attribute encoding its slots as time;price;childprice;seats. Any slot with seats above zero is available. That was the whole signal.
From there it was straightforward: poll the current month plus the next two, every five minutes, parse out every day and every slot, and compare against what we saw last time. If a seat appeared, send an email immediately with a link that opened the booking form directly on that slot. If a whole new month got published, say so. And once a day, report in.
Writing something that checks a page is a twenty-minute job. Writing something you will still trust at 3am on day nine is not. The difference is entirely in the parts nobody demos:
flowchart LR
classDef extern fill:#fdfcfb,stroke:#c4873b,stroke-width:1.5px,color:#0a0f0f
classDef hub fill:#0a0f0f,stroke:#c4873b,stroke-width:1.5px,color:#fdfcfb
classDef mail fill:#f3f2f1,stroke:#9a9a96,color:#0a0f0f
S["Consulate
booking calendar"]:::extern
N["Watcher
every 5 min"]:::hub
D{"Seats > 0?
New day?"}:::hub
A["Instant alert
+ booking link"]:::mail
H["Daily summary
(heartbeat)"]:::mail
S -->|3 months of HTML| N
N --> D
D -->|yes| A
D -->|no| H
Three months of calendar polled every five minutes; anything new goes out instantly, everything else rolls into a once-a-day heartbeat.
It ran for eleven days and did roughly three thousand checks. Not one of them failed. It also never sent a single alert — and that is the interesting part.
Because the daily summary reported capacity as well as activity, we could see the shape of the thing precisely. Each run scanned a rolling three-month window: 92 days, of which 65 were weekdays that could plausibly hold appointments. The consulate had published 19. All 19 were fully booked — and stayed fully booked, across every one of those three thousand consecutive observations. The two later months in the window were never published at all.
Cancellations were certainly happening. People’s plans change. But not one freed seat ever reached the public calendar, not once in eleven days of looking every five minutes. That is not a page we were checking too slowly. That is a page that does not carry the information.
Once you can say that with confidence — not “we haven’t got lucky yet” but “we have measured this continuously and the signal does not exist” — the problem changes completely. There is no point being faster at watching an empty channel. The seats were clearly being allocated somewhere else.
So instead of waiting, she wrote to the consulate directly: who she was, why she needed to travel, by when. They replied with an appointment. It turns out that is simply how they release capacity — held back from the public calendar and given out by hand to people who ask and explain themselves.
The watcher never sent the alert it was built to send. What it produced instead was the evidence to stop refreshing and go and ask a human. That was worth considerably more.
The day the appointment was confirmed, we decommissioned it — workflow deleted, source removed, the lot. An automation with no job left is not a neutral thing to leave running; it is unattended code making requests to someone else’s server forever, and one more thing that can fail confusingly at a moment when nobody remembers why it exists.
The lesson we kept is this: before you automate a watcher, confirm the channel actually carries the thing you are waiting for. A quiet feed and a dead feed look identical from the outside. The only way to tell them apart is to measure — or, far cheaper, to ask the source how it really works before you write any code at all.
Got a process where you’re the one doing the refreshing, checking, or chasing? The first automation is free — and if the honest answer turns out to be “don’t automate this, do that instead”, we’ll tell you that too. Start a conversation →