Watching seventy cities with one morning trigger

July 5, 2026

A proof of concept in automated situational awareness: every morning, one trigger checks seventy US cities for weather trouble, rising respiratory illness, and recalls, then explains what it found.

Late last year I wanted to find out how much situational awareness one person can automate. The test I settled on: pick around seventy US cities, spread across most of the country, and check every one of them, every morning, for three kinds of trouble. Weather that's coming. Respiratory illness that's rising. Products being recalled. Then score what turns up, explain it in plain language, and write it down, with no human anywhere in the loop.

This was a proof of concept, and this post is an overview of its architecture rather than a code walkthrough. What I want to show is what the government's free data will actually give you, where it resists you, and how little glue it takes to turn it into something that feels like a service.

One trigger, three pipelines

The whole system was orchestrated in n8n and hung off a single time trigger. Every morning it fired once, and everything else followed: pull the feeds, run each domain's scoring, have a model explain anything that scored high enough, and land every alert as a row in Airtable. That row was the product. By the time anyone was drinking coffee, the morning's picture of all seventy cities was sitting in a table.

One morning triggern8n, dailyPull the feedsNWS, CDC, openFDAScore each domaineach on its own termsExplain each alertLLM, given the scoring logicWrite rows to Airtablethe whole output
The daily run. One trigger at the top, rows in a table at the bottom, nothing manual in between.

The three domains share almost nothing. Different sources, different units, different ideas of severity. The only thing they have in common is the question the system exists to answer: is anything happening in this city that someone would want a heads-up about?

Well, that and one other thing. They all have to agree on what "this city" means.

Geography is the hard part

Every federal data source speaks a different dialect of "where." Before the system could score anything, it had to translate a list of seventy cities into each source's native geography, and this turned out to be the real engineering of the project.

The spine I used was the county FIPS code, the five-digit federal identifier for every US county. Each city on the list maps to the county it sits in, and from there each source gets its own translation:

~70 citiesCounty FIPSthe spine keyNWS alertsSAME code = 0 + FIPSWastewatercounty names, no FIPSED visitsHSA regionsFDA recallsfree text, state at best
One list of cities, one spine key, four dialects of geography. The recall feed is the degraded one.

Weather is the clean one. Every alert from the National Weather Service carries SAME geocodes, and a SAME code is literally a zero prepended to the county FIPS. You can also query active alerts by county code directly:

GET https://api.weather.gov/alerts/active?zone=TXC375

That's Potter County, Texas. The API needs no key at all, just a User-Agent header that says who you are.

Health data is where the dialects start drifting. CDC publishes its surveillance data on data.cdc.gov behind the Socrata API, and I used two feeds. The wastewater surveillance data reports a viral activity level per monitoring site for COVID-19, influenza A, and RSV, but it identifies locations by county name, so the join happens on names and states rather than FIPS. The emergency department visit data reports what percentage of ED visits in a region are for each of those same three illnesses, and its regions are Health Service Areas, clusters of counties that share a hospital market. Each dataset publishes which counties an HSA contains, so FIPS gets you in, just not directly.

Finally, with recalls, county level is typically unrealistic. openFDA enforcement records have no county field, no FIPS, nothing structured below the recalling firm's own address. What they have is a free-text field called distribution_pattern, and it contains whatever the firm wrote down. Real values look like "Nationwide", or "FL, MI, MS, and OH.", or a four-hundred-character paragraph listing countries. The best you can honestly extract is state mentions, so that's what the system did: parse states out of the text, treat "nationwide" as everyone, and accept that recall geography bottoms out at the state level. If a recall hit Texas, every Texas city on the list got it.

One more honest note on coverage: wastewater monitoring only exists where someone installed a sampler. There are roughly 1,500 monitored sites in the country, and some of my seventy cities had no wastewater signal at all. The system just had less to say about those.

Weather: relay what's official, generate what isn't

The weather pipeline did two different jobs.

The first was relaying. The NWS alert feed already carries every watch, warning, and advisory in the country, each with structured severity, urgency, and certainty fields, and severe weather watches from the Storm Prediction Center flow through the same feed. When a tornado warning covers your county, no algorithm of mine needs an opinion. The system passed these through with the official severity attached.

The second job was the interesting one: generating alerts for conditions the NWS doesn't headline. A single thunderstorm gets a warning. Six straight days of rain might not (if the conditions are not severe enough), and neither does a slow-building stretch of extreme heat or a drought. So for each city the system also pulled the forecast, which the NWS serves as a numeric time series about a week out, and ran simple heuristics over the window: sustained rain, sustained heat, the long dry stretches. The bar for generating an alert was the question: "Is this worth telling someone about a week before it matters?"

Health: a level and a slope

The health signals are the reason I'm glad I built this. Wastewater viral activity and ED visit percentages are genuinely good data, updated weekly, covering three illnesses that actually shape how bad a city's month is about to be.

Both signals arrive already normalized. Wastewater activity is reported as a level relative to that site's own history, and ED visits as a percentage of all visits. So the scoring didn't need to invent a scale, it needed to answer two questions per city, per illness. How high is the level? And which way is it moving?

Either question alone can justify an alert. A high enough level is alarming even if it's flat, and a steep enough rise is worth watching even from a low base. The system computed the trend as a slope over a rolling window of two weeks, and the two factors together decided the category: quiet, watch, or urgent.

Early risewatch, from a low baseUrgenthigh and climbingQuietno alertElevated, stablewatch, already highnear baselineelevatedLevelflatrisingTrend
Illustrative. Two factors per signal, and either one alone can earn an alert. The corner where both are high is the one that matters.

The wastewater and ED visit datasets publish weekly, and so do FDA enforcement reports. Only the weather changes daily. So the daily trigger earned its keep on forecasts and warnings, and on the health and recall side it mostly confirmed that nothing new had landed, then caught the weekly updates the morning they appeared. A pipeline that runs cheerfully even when most of its inputs haven't moved is not wasted work. It's what lets you never think about it.

Recalls: take the FDA's word for it

The recall pipeline was deliberately the dumbest of the three. openFDA's enforcement endpoints cover food and drug recalls, and every record arrives already classified by the FDA: Class I for the recalls that can kill you, Class II for the ones that can hurt you, Class III for the ones that mostly hurt the firm. I saw no reason to be cleverer than the agency whose job this is, so the system passed the classification straight through as the alert's severity.

All the pipeline added was the geographic scoping from earlier, parsing states out of distribution_pattern, plus a date-range query to pick up what was new:

GET https://api.fda.gov/food/enforcement.json
    ?search=report_date:[20251201+TO+20251231]

A Class I recall whose distribution text mentioned your state showed up in your city's morning rows, worded carefully as "distributed to your state," because claiming anything more precise would be inventing data the FDA doesn't publish.

The model comes last

There's an instinct right now to put the model at the front of a system like this and let it decide what matters. I did the opposite. Everything that decided whether an alert existed, and how severe it was, was ordinary statistics: joins, thresholds, slopes, a classification passed through. Deterministic, cheap, debuggable.

The model's job was the last mile. For each alert that the scoring produced, the OpenAI API got the alert's numbers along with a description of how the scoring works, and wrote the plain-language explanation: what fired, why, what the level and trend actually mean for that city. The kind of paragraph a person would write if a person were willing to write seventy of them before breakfast, every day, forever.

That division of labor is most of why the system could run unattended. A model that explains a score can be wrong in ways that embarrass you. A model that produces the score can be wrong in ways you'll never notice.

What it proved

The proof of concept did what I wanted it to prove. Multi-city, multi-domain situational awareness, the kind of thing that sounds like it needs a team and a budget, is a one-person build on free government data. The feeds are good. The geography is annoying but solvable with one honest spine key and a tolerance for each source's dialect. The statistics are simple. The model earns its place by explaining, not deciding.

And the part I remain proudest of has nothing to do with any single piece of it. It's that every morning, all of that data got pulled, joined, scored, explained, and filed for seventy cities at once, and nobody, including me, had to touch a thing.