Engineering · August 28, 2026

We built a listings site in 18 days that updates itself every morning

WhereToGoNYC went from an empty repository to a live site in 18 days and 190 commits. It lists NYC nightlife events, and a pipeline rebuilds the board unattended every morning. The interesting part is not the build. It is the morning the pipeline was quietly failing and every test we had said it was fine.

What it does every morning

WhereToGoNYC is ours. Nightlife listings go stale faster than almost any other content — lineups change, ticket links die, events move. Keeping a board accurate by hand is a daily job nobody wants. So the pipeline does three things before anyone is awake.

Assembles the night’s listings. Pulls together the events for the upcoming nights and stages them for the public board.

Validates every ticket link. Each link gets checked. A listing whose ticket destination cannot be confirmed does not go live pointing at a dead page.

Reads the flyer. Every event flyer is handed to an AI vision model that reads the printed artwork and confirms it matches the event’s date and lineup. This is the check that a filename or a database field cannot do — the poster is the thing the customer actually believes, and it is the thing most likely to be stale.

The rule underneath all three: anything the pipeline cannot verify stays a draft. It does not publish a guess. Wrong information on a listings site is worse than no listing, because a wrong door time sends a real person to a real venue at the wrong hour.

What it produced

In one evening the verified board went from 9 published nights to 60. One morning’s unattended run published 38 events. A scheduled job handles it daily. Nobody sits down to update the site.

Then the 07:00 job stopped working, and said nothing

The scheduled 07:00 run was failing. Every single morning. Silently. The job was installed, the schedule was correct, the code was correct, and every morning it produced nothing.

Every time we investigated, we did the reasonable thing: opened a terminal and ran the same check by hand. It passed. Clean, green, every time. Which pushed us toward every wrong theory available — the job must not be installed, the schedule must be wrong, something environmental and vague.

The actual cause: the machine had two versions of Node installed. The scheduler picked up an x86 build running under Rosetta, while the project’s dependencies were compiled for arm64. Native modules built for one architecture cannot load in the other, so the run died on startup, every time, before it could do anything worth logging.

It hid because of one detail: an interactive shell resolves a different PATH than the scheduler does. Running it by hand picked the correct Node. The scheduler picked the wrong one. Same command, same machine, same repository, two different programs actually executing. Our test was not inconclusive — it was confidently, repeatably testing something other than the thing that was broken.

The lesson we paid for

A green local test is not evidence about a scheduled job. It is evidence about your shell.

Different user. Different PATH. Different environment variables. Different machine architecture, if you are on a Mac that can run both. The scheduler is a different execution context than your terminal, and any of those differences is enough to make your passing test irrelevant to the thing that is failing.

Test the thing you actually ship, in the environment it actually runs in. If the code runs at 07:00 under a scheduler, the only test that counts is one that runs under the scheduler. Everything else is a comforting rehearsal.

Automation you do not monitor is a rumor

This is the real point. The pipeline was described as “running every morning.” That description was based on the fact that we built it and installed it. It had become a story we told ourselves rather than a fact anyone had confirmed.

A scheduled job that fails silently is indistinguishable from a scheduled job that works, right up until someone happens to look at the output and count. Silence is not success. Silence is the absence of information, and unattended systems produce a lot of it by design.

The answer is not more discipline about checking. The answer is building the verification into the pipeline so the system is responsible for proving its own work. That is why the flyer vision check and the link validation exist: not as features on top, but as the pipeline confirming what it is about to claim before it claims it publicly. Anything unproven stays a draft. That principle should apply to the run itself as much as to the events.

What to check on your own systems

  • Confirm every scheduled job actually ran today. Not that it is installed — that it ran and produced output. If you cannot answer that in under a minute, you do not know whether your automation is working.
  • Make failure loud. A job that fails silently is the worst possible design. Alert on the absence of a successful run, not just on errors, because the bad cases often die before they can raise one.
  • Check which interpreter your scheduler resolves. Have the job print the full path and version of its runtime on every run. If that differs from what your terminal uses, you have found tomorrow’s outage today.
  • Never accept a passing manual run as proof about an automated one. Reproduce the failure in the scheduler’s context, or you are debugging a different program.
  • Put verification inside the pipeline, not beside it. Anything the system cannot verify should stay unpublished by default. Publishing wrong information confidently is worse than publishing nothing.

Why 18 days is the boring number

Building the site fast is not the achievement. Plenty of things get built fast and then rot, because keeping them accurate turns into a daily human job that nobody does forever. The part worth building is the part that checks itself — and the part that tells you, out loud, when it could not.

See how this system is built

Do you know whether your automation ran this morning?

Book a build consult