--- name: nws-weather-scraper description: Read US National Weather Service data as one flat record per location via the Apify Actor arman-bd/nws-weather-scraper. Returns twelve-hour or hourly forecast periods, the latest reading from the nearest observation station, and every active watch or warning covering the point, alongside the resolved grid cell, forecast office, timezone and radar station. Use when a task needs US forecasts for a list of sites, severe-weather alerting on a schedule, or weather features joined to operational or sales data. Not for locations outside US coverage, historical or climatological records, or radar and satellite imagery. --- # NWS Weather Scraper Apify Actor `arman-bd/nws-weather-scraper`. Give it coordinates or NWS zone codes and pick a product, get one dataset record per location. Every record carries the same keys whichever product ran, so the dataset stays flat and stable. No credentials are involved. ## When to use it - Tomorrow's conditions across a list of depots, stores or job sites, in one run. - Severe-weather alerting: poll with `dataType: "alerts"` and page on Severe or Extreme events. - Weather features for a model: daily highs per site, joined to sales or demand history. - Current conditions from the nearest official station, rather than a modelled estimate. - A warning history for a portfolio of insured or monitored sites, built by scheduling the run and keeping every dataset. ## When not to use it - Anywhere outside US states, territories and coastal waters. Other coordinates are rejected per location. - Historical weather, climate normals or reanalysis. Only current products are served. - Radar imagery, satellite imagery, model grids or soundings. - Sub-hourly nowcasting. Forecast products are re-issued a few times a day and station observations update roughly hourly. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/nws-weather-scraper').call({ locations: ['40.7128,-74.0060', '47.6062,-122.3321'], dataType: 'forecast', units: 'us', includeAlerts: true, maxPeriods: 8, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~nws-weather-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"locations":["40.7128,-74.0060","41.8781,-87.6298"],"dataType":"forecast","units":"us","maxPeriods":8}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/nws-weather-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `locations` | string[] | yes | | `"lat,lon"` strings or NWS zone codes such as `KSZ009` (forecast zone) or `KSC201` (county zone). A full zone URL works, since the last path segment is taken. Mixed input is fine. Coordinates are rounded to four decimals and duplicates are dropped. | | `dataType` | string | no | `forecast` | One of `forecast` (twelve-hour periods), `hourly` (hour by hour), `alerts` (active warnings only, no forecast), `observations` (latest reading from the nearest station). Any other value falls back to `forecast`. | | `units` | string | no | `us` | `us` for Fahrenheit, mph and inches of mercury; `si` for Celsius, km/h and pascals. Anything other than `si` is treated as `us`. | | `includeAlerts` | boolean | no | `true` | Attach every active alert covering the location. Forced on when `dataType` is `alerts`. Turning it off saves one request per location. | | `maxPeriods` | integer | no | `24` | Cap on stored forecast periods per location. `0` keeps every period. Hourly forecasts return roughly 156 periods, which is why the default is low. | | `contactEmail` | string | no | `""` | Optional address added to the identifying header the Actor sends, so the service can reach you about heavy usage. Set it on anything scheduled. | **Point input and zone input are not interchangeable, and that is the choice that matters.** A coordinate resolves to a grid cell and returns structured numbers: `temperature`, `gridId`, an hourly variant, and unit conversion. A zone code returns the forecaster's narrative text for an official area, with no numeric temperature, no hourly variant, and `units` ignored. Pass coordinates whenever you need values you can compute with, and reserve zone codes for alerting and for matching NWS's own areas. ## Output One record per location that resolved. Fields not applicable to the product you chose are `null` or empty rather than absent. | Field | Type | Notes | |---|---|---| | `location` | string | The normalised input: `lat,lon` rounded to four decimals, or the uppercased zone code. Join back to your input on this. | | `locationType` | string | `point` or `zone`. | | `latitude` | number \| null | As sent, rounded to four decimals. `null` for zone codes. | | `longitude` | number \| null | As sent. `null` for zone codes. | | `city` | string \| null | Nearest named place for a point; the zone's own name for a zone. | | `state` | string \| null | Two-letter state code. | | `gridId` | string \| null | Forecast grid identifier. Points only. | | `gridX` | number \| null | Grid column. Points only. | | `gridY` | number \| null | Grid row. Points only. | | `forecastOffice` | string \| null | Issuing weather forecast office. Points only. | | `timeZone` | string \| null | IANA timezone, for example `America/New_York`. | | `radarStation` | string \| null | Covering radar site. | | `dataType` | string | The product that ran, echoed back. | | `units` | string | The unit system that ran, echoed back. | | `temperature` | number \| null | Headline temperature from the first forecast period. `null` for zones and for the `alerts` and `observations` products. | | `temperatureUnit` | string \| null | `F` or `C`, matching `units`. | | `windSpeed` | string \| null | Text, for example `6 to 10 mph`, not a number. | | `windDirection` | string \| null | Compass text, for example `SW`. | | `shortForecast` | string \| null | One-line summary for the first period. | | `detailedForecast` | string \| null | Full narrative for the first period. | | `forecastPeriods` | object[] | The source's own period objects, passed through unchanged and capped by `maxPeriods`. Keys include `number`, `name`, `startTime`, `endTime`, `isDaytime`, `temperature`, `temperatureUnit`, `probabilityOfPrecipitation`, `windSpeed`, `windDirection`, `shortForecast`, `detailedForecast`. Empty for the `alerts` and `observations` products. | | `periodCount` | number | Length of `forecastPeriods` after capping, not the number available. | | `observationStation` | object \| null | Populated only by the `observations` product. Keys `stationId`, `stationName`, `timestamp`, `textDescription`, plus `temperature`, `dewpoint`, `relativeHumidity`, `windSpeed`, `windDirection`, `windGust`, `barometricPressure` and `visibility`, each either `null` or `{ value, unit }` rounded to two decimals. | | `alerts` | object[] | Active alerts covering the location. Keys `id`, `event`, `severity`, `urgency`, `certainty`, `headline`, `areaDesc`, `onset`, `expires`, `senderName`, `description`. Empty when `includeAlerts` is off. | | `alertCount` | number | Length of `alerts`. | | `forecastUrl` | string \| null | The product URL that was fetched. `null` for the `alerts` and `observations` products. | | `updatedAt` | string \| null | Timestamp the service stamped on the product, or the observation time in the `observations` product. | | `scrapedAt` | string | Run timestamp, ISO 8601 UTC. | A real record, arrays trimmed: ```json { "location": "40.7128,-74.006", "locationType": "point", "latitude": 40.7128, "longitude": -74.006, "city": "New York", "state": "NY", "gridId": "OKX", "gridX": 33, "gridY": 42, "forecastOffice": "OKX", "timeZone": "America/New_York", "radarStation": "KDIX", "dataType": "forecast", "units": "us", "temperature": 90, "temperatureUnit": "F", "windSpeed": "6 to 10 mph", "windDirection": "SW", "shortForecast": "Mostly Sunny", "detailedForecast": "Mostly sunny, with a high near 90. Heat index values as high as 102. …", "forecastPeriods": [ { "number": 1, "name": "Today", "startTime": "2026-08-06T07:00:00-04:00", "endTime": "2026-08-06T18:00:00-04:00", "isDaytime": true, "temperature": 90, "temperatureUnit": "F", "probabilityOfPrecipitation": { "unitCode": "wmoUnit:percent", "value": 9 }, "windSpeed": "6 to 10 mph", "windDirection": "SW", "shortForecast": "Mostly Sunny", "detailedForecast": "Mostly sunny, with a high near 90." } ], "periodCount": 4, "observationStation": null, "alerts": [ { "id": "urn:oid:2.49.0.1.840.0.c1834a44913d904db6ce91b95b70d126406790b2.004.1", "event": "Heat Advisory", "severity": "Moderate", "urgency": "Expected", "certainty": "Likely", "headline": "Heat Advisory issued August 6 at 2:26AM EDT until August 7 at 8:00PM EDT by NWS Upton NY", "areaDesc": "New York (Manhattan); Bronx; Richmond (Staten Is.); Kings (Brooklyn); …", "onset": "2026-08-06T11:00:00-04:00", "expires": "2026-08-06T10:30:00-04:00", "senderName": "NWS Upton NY", "description": "* WHAT.Heat index values up to 104 expected." } ], "alertCount": 1, "forecastUrl": "https://api.weather.gov/gridpoints/OKX/33,42/forecast?units=us", "updatedAt": "2026-08-06T09:41:52+00:00", "scrapedAt": "2026-08-06T11:35:21.410Z" } ``` With `dataType: "observations"`, `forecastPeriods` is empty and `observationStation` carries the reading: ```json { "stationId": "KMYZ", "stationName": "Marysville Municipal Airport", "timestamp": "2026-08-06T10:55:00+00:00", "textDescription": "Fog/Mist", "temperature": { "value": 68, "unit": "F" }, "dewpoint": { "value": 64.4, "unit": "F" }, "relativeHumidity": { "value": 88.27, "unit": "%" }, "windSpeed": { "value": 4.7, "unit": "mph" }, "windDirection": { "value": 80, "unit": "deg" }, "windGust": null, "barometricPressure": { "value": 30.03, "unit": "inHg" }, "visibility": { "value": 3, "unit": "mi" } } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "locationsRequested": 3, "locationsFailed": 1, "failures": [ { "location": "51.5074,-0.1278", "error": "not covered by NWS (404): …" } ], "recordsSaved": 2, "filters": { "dataType": "forecast", "units": "us", "includeAlerts": true, "maxPeriods": 8 }, "finishedAt": "2026-08-06T11:35:22.104Z" } ``` `locationsRequested` counts the locations that *parsed*, after unrecognised strings were discarded and duplicates removed, so it can be smaller than the array you sent and the difference is visible only in the run log. `recordsSaved` plus `locationsFailed` should equal `locationsRequested`; each entry in `failures` names the normalised location and the reason. Note that `filters` does not echo `locations` or `contactEmail`. ## Behaviour to plan around - **A malformed location vanishes without a failure entry.** Anything that is neither `lat,lon` nor a zone code is logged and dropped before the run starts. It never reaches `failures`, so reconcile the dataset against your input list on `location` rather than trusting the counts. - **Coordinates are rounded to four decimals**, and `location` echoes the rounded value. Pass `-74.0060` and you get back `-74.006`, so compare as numbers, not as strings. - **Zone codes return text, not numbers.** `temperature` is `null`, `units` is ignored, and `gridId`, `gridX`, `gridY` and `forecastOffice` stay `null`. Asking for `hourly` with a zone code quietly returns the standard zone narrative, because there is no hourly variant for a zone. - **`windSpeed` on a forecast is a string.** `"6 to 10 mph"` is a range, not a number. Parse it if you need a value, or use the `observations` product, where `windSpeed` is a `{ value, unit }` object. - **`periodCount` is what was kept, not what was offered.** With `maxPeriods: 24` an hourly forecast reports 24 even though far more periods were available. The headline fields always come from the first period regardless of the cap. - **`forecastUrl` is null for the alerts and observations products.** Do not use it as a presence check for a successful record. - **The alerts product still resolves the location first**, so it carries `city`, `state` and the grid fields alongside `alerts`, with all forecast fields empty. - **Requests per location vary by product**: two for a forecast without alerts, three with alerts, four for observations with alerts. Turning `includeAlerts` off is a real saving on a large list. - **Transient failures are retried** three times with linear backoff. A location outside coverage and a rejected request are final for that location and are not retried. - **The run only throws when every location failed.** Any surviving location makes the run succeed with a non-empty `failures` list. ## Recipes **Daily operational forecast across sites.** Coordinates, twelve-hour periods, alerts on. ```json { "locations": ["40.7128,-74.0060", "41.8781,-87.6298", "29.7604,-95.3698"], "dataType": "forecast", "units": "us", "maxPeriods": 4, "contactEmail": "ops@example.com" } ``` Read `temperature`, `shortForecast` and `alertCount` per site for a dispatch board, and drill into `forecastPeriods` only where `alertCount` is above zero. **Severe-weather sweep on a schedule.** Zone codes, alerts only, so the records are small. ```json { "locations": ["KSZ009", "TXZ248", "FLZ173"], "dataType": "alerts", "contactEmail": "ops@example.com" } ``` Flatten `alerts` and page on `severity` of `Severe` or `Extreme`. De-duplicate on the alert `id` across runs so a multi-hour warning fires once. **Hourly detail in metric.** Alerts off keeps the run to two requests per location. ```json { "locations": ["47.6062,-122.3321", "37.7749,-122.4194"], "dataType": "hourly", "units": "si", "includeAlerts": false, "maxPeriods": 24 } ``` Each entry in `forecastPeriods` carries `startTime` and `endTime` with an offset, so convert to UTC before joining across sites in different `timeZone` values. **Current conditions from the nearest station.** ```json { "locations": ["39.7456,-97.0892"], "dataType": "observations", "units": "us" } ``` `observationStation.timestamp` is the reading time and can be up to an hour old. Individual measurements are `null` when that station does not report them, which is common for `windGust`.