--- name: open-meteo-weather-scraper description: Read global weather for any coordinate or place name via the Apify Actor arman-bd/open-meteo-weather-scraper. Returns one record per location holding current conditions with a decoded weather description, column-oriented hourly and daily series out to 16 days, or reanalysis history back to 1940 when a start date is given, plus the resolved grid cell, its elevation, the timezone and the unit of every variable. Use when a task needs forecasts for a list of places, weather features for a model, or a long historical series for a backtest. Not for severe-weather alerts, radar imagery, air quality, marine or official government forecast products. --- # Open-Meteo Weather Scraper Apify Actor `arman-bd/open-meteo-weather-scraper`. One dataset record per location, in one of two modes: a live forecast, or a reanalysis archive stretching back to 1940. It takes no credentials of your own. Coordinates and place names are both accepted, so `"Tokyo"` works as well as `"35.68,139.69"`. ## When to use it - A list of sites (stores, farms, venues, postcodes) needs a forecast refreshed daily. - You are backtesting something against weather and need years of daily history. - You want hourly temperature, cloud cover or wind as features for a demand model. - You need soil temperature and moisture series for agricultural monitoring. - Any of the above for many places at once, aligned to a single timezone. ## When not to use it - Warnings, watches or severe-weather alerts. None are returned. - Radar, satellite imagery or any other raster product. - Air quality, marine, wave or river data. Those are separate products. - Authoritative national forecast text, which comes from a government service rather than a model reanalysis. Some users run both and compare. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/open-meteo-weather-scraper').call({ locations: ['52.52,13.41', 'Tokyo'], variables: ['temperature_2m', 'precipitation'], dailyVariables: ['temperature_2m_max', 'temperature_2m_min', 'precipitation_sum'], forecastDays: 7, timezone: 'UTC', units: 'metric', }); 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~open-meteo-weather-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"locations":["52.52,13.41","Tokyo"],"dailyVariables":["temperature_2m_max","precipitation_sum"],"forecastDays":7,"timezone":"UTC"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/open-meteo-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 place names. Anything not matching the coordinate pattern is treated as a place name and geocoded to the single best match. Coordinates outside ±90 or ±180 are skipped with a warning. Deduplicated case-insensitively on the string you passed. | | `variables` | string[] | no | `["temperature_2m","relative_humidity_2m","precipitation","wind_speed_10m"]` | Hourly variable names. An empty array skips the hourly series entirely. | | `dailyVariables` | string[] | no | `["weather_code","temperature_2m_max","temperature_2m_min","precipitation_sum"]` | Daily aggregate names, which differ from the hourly ones. An empty array skips the daily series. | | `forecastDays` | integer | no | `3` | 1 to 16, clamped into range. Ignored entirely in historical mode. | | `historicalFrom` | string | no | `""` | `YYYY-MM-DD`. **Setting this switches the whole run to the archive.** Any other format throws before a request is made. | | `historicalTo` | string | no | `""` | `YYYY-MM-DD`. Only used alongside a start date, and defaults to yesterday when omitted. A start after the end throws. | | `timezone` | string | no | `"auto"` | IANA name such as `Europe/Berlin`, or `auto` for each location's own local time. | | `units` | string | no | `"metric"` | `metric` for Celsius, km/h and millimetres, or `imperial` for Fahrenheit, mph and inches. The exact unit per variable comes back in `units`. | **`historicalFrom` is the mode switch, and it changes where the data lands.** With it set, every `current*` field is `null`, `hourly` and `daily` are `null`, and the series arrive under `historicalSeries` instead; `forecastDays` is ignored. Without it you get the forecast shape and `historicalSeries` is `null`. The other decision is naming: hourly and daily variables use different names (`temperature_2m` hourly, `temperature_2m_max` daily), and putting a daily name in `variables` gets the whole request for that location rejected, with the reason copied verbatim into `RUN_SUMMARY.failures`. ## Output One record per location that resolved. | Field | Type | Notes | |---|---|---| | `location` | string | Exactly the string you passed, normalised for coordinate inputs. Join back to your input list on this. | | `resolvedName` | string \| null | Geocoder match. `null` when you passed coordinates. | | `admin1` | string \| null | First-level administrative area of the match. `null` for coordinate inputs. | | `country` | string \| null | Country name of the match. `null` for coordinate inputs. | | `countryCode` | string \| null | Country code of the match. `null` for coordinate inputs. | | `latitude` | number | The grid cell actually served, not necessarily what you asked for. | | `longitude` | number | Same. | | `elevation` | number \| null | Elevation of that cell, in metres. | | `timezone` | string \| null | Timezone the series timestamps are expressed in. | | `timezoneAbbreviation` | string \| null | For example `GMT+9`. | | `utcOffsetSeconds` | number \| null | Offset applied to the timestamps. | | `mode` | string | `forecast` or `historical`. Branch on this before reading any series. | | `currentTime` | string \| null | Timestamp of the current observation. Forecast mode only. | | `currentTemperature` | number \| null | Forecast mode only. | | `currentApparentTemperature` | number \| null | Forecast mode only. | | `currentRelativeHumidity` | number \| null | Forecast mode only. | | `currentPrecipitation` | number \| null | Forecast mode only. | | `currentWindSpeed` | number \| null | Forecast mode only. | | `currentWindDirection` | number \| null | Degrees. Forecast mode only. | | `currentWeatherCode` | number \| null | WMO code. Forecast mode only. | | `currentWeatherDescription` | string \| null | Plain-English meaning of the code, or `Unknown` for a code outside the table. | | `hourly` | object \| null | Column-oriented forecast series: a `time` array plus one array per requested variable, all the same length. `null` in historical mode or when `variables` is empty. | | `daily` | object \| null | Same shape for daily aggregates. `null` in historical mode or when `dailyVariables` is empty. | | `historicalSeries` | object \| null | `{ hourly, daily }` with the same column-oriented shape. `null` in forecast mode. | | `units` | object | `{ current, hourly, daily }`, each a variable-to-unit map or `null`. Read it rather than assuming, since `units: "imperial"` changes them all. | | `sourceUrl` | string | The exact request URL behind this record, so any row can be reproduced by hand. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real forecast record, series trimmed: ```json { "location": "Tokyo", "resolvedName": "Tokyo", "admin1": "Tokyo", "country": "Japan", "countryCode": "JP", "latitude": 35.7, "longitude": 139.6875, "elevation": 40, "timezone": "Asia/Tokyo", "timezoneAbbreviation": "GMT+9", "utcOffsetSeconds": 32400, "mode": "forecast", "currentTime": "2026-08-06T20:30", "currentTemperature": 27.1, "currentApparentTemperature": 33.6, "currentRelativeHumidity": 91, "currentPrecipitation": 0, "currentWindSpeed": 3.3, "currentWindDirection": 139, "currentWeatherCode": 1, "currentWeatherDescription": "Mainly clear", "hourly": { "time": ["2026-08-06T00:00", "2026-08-06T01:00", "2026-08-06T02:00"], "temperature_2m": [23.8, 23.3, 23.0], "precipitation": [0.2, 0.3, 0.3] }, "daily": { "time": ["2026-08-06", "2026-08-07"], "weather_code": [51, 53], "temperature_2m_max": [31.1, 30.7], "temperature_2m_min": [22.6, 24.7], "precipitation_sum": [2.0, 3.1] }, "historicalSeries": null, "units": { "current": { "temperature_2m": "°C", "wind_speed_10m": "km/h" }, "hourly": { "time": "iso8601", "temperature_2m": "°C", "precipitation": "mm" }, "daily": { "time": "iso8601", "temperature_2m_max": "°C", "precipitation_sum": "mm" } }, "sourceUrl": "https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.69171&…", "scrapedAt": "2026-08-06T11:39:49.134Z" } ``` Series are column-oriented, so index `i` of every array in a block describes the same instant. Flatten with the `time` array as the spine: ```js const rows = record.daily.time.map((t, i) => ({ date: t, max: record.daily.temperature_2m_max[i], min: record.daily.temperature_2m_min[i], rain: record.daily.precipitation_sum[i], })); ``` ## 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": "Atlantis", "error": "no place matched \"Atlantis\"" } ], "recordsSaved": 2, "filters": { "mode": "forecast", "historicalFrom": null, "historicalTo": null, "forecastDays": 7, "variables": ["temperature_2m", "precipitation"], "dailyVariables": ["temperature_2m_max", "precipitation_sum"], "timezone": "UTC", "units": "metric" }, "finishedAt": "2026-08-06T11:39:52.004Z" } ``` `locationsRequested` counts what survived normalisation and deduplication, not what you sent, so an entry rejected as unusable never appears anywhere in the summary. Check that number against your own list length first. `failures` then explains every gap between it and `recordsSaved`, and its `error` string carries the source's own reason, which is how a mistyped variable name is diagnosed. `filters.mode` confirms which shape the records are in, and `filters.historicalTo` shows the end date that was defaulted in. ## Behaviour to plan around - **The mode decides which fields are populated,** and reading the wrong one silently yields `null`. Branch on `mode`, or on `historicalSeries !== null`, before touching a series. - **Coordinates snap to a model grid.** The `latitude` and `longitude` in the record are the cell centre, typically a few kilometres from what you asked for, and `elevation` is that cell's elevation. This matters when comparing a valley station with a mountain one. - **Place names resolve to a single best match, with no disambiguation.** There are many places called Springfield. Pass coordinates when the exact point matters, and check `resolvedName`, `admin1` and `country` when you did not. - **The current-conditions block is a fixed set of variables** and is not affected by `variables`. Changing `variables` changes `hourly` only. - **A bad variable name fails that location, not the run.** The request is rejected outright, the reason is recorded in `failures`, and the other locations continue. The Actor only throws when every location failed. - **Hourly and daily namespaces are different.** `temperature_2m` is hourly, `temperature_2m_max` is daily. Neither list is validated locally, so a typo is only visible in `failures`. - **`historicalTo` defaults to yesterday, not today,** because the reanalysis lags real time by a few days. A run asking for today's archive data gets nothing for the recent end of the range. - **One location is one request, and place names cost two.** Locations are fetched sequentially, so wall time grows linearly with the list and a hundred place names is two hundred requests. - **Units change with `units`, including the strings in `units`.** Never hard-code `°C` or `mm`; read the map. Wind direction is degrees in both systems. - **`timezone: "auto"` makes series incomparable across locations,** because each one is in its own local time. Pass `UTC` whenever you intend to join or average across places. - **Transient failures are retried** three times with linear backoff. A rejected request is final and is not retried. ## Recipes **Daily outlook for a list of sites.** Defaults are close to right; pin the timezone so the rows line up. ```json { "locations": ["52.52,13.41", "Tokyo", "-33.8688,151.2093"], "forecastDays": 7, "timezone": "UTC" } ``` Flatten `daily` with the `time` array and join to your site table on `location`. **Model features, hourly, two weeks out.** No daily block, so the payload stays lean. ```json { "locations": ["40.7128,-74.0060", "51.5072,-0.1276"], "variables": ["temperature_2m", "precipitation", "cloud_cover", "wind_speed_10m"], "dailyVariables": [], "forecastDays": 14, "timezone": "UTC", "units": "metric" } ``` Index `hourly` by its `time` array and feed the columns straight into a feature frame. **Ten years of daily history for a backtest.** Hourly off, because a decade of hourly rows is enormous and rarely what a backtest needs. ```json { "locations": ["Berlin", "Paris", "Madrid"], "variables": [], "dailyVariables": ["temperature_2m_max", "temperature_2m_min", "precipitation_sum", "sunshine_duration"], "historicalFrom": "2015-01-01", "historicalTo": "2024-12-31", "timezone": "UTC" } ``` Read `historicalSeries.daily`, not `daily`, and expect every `current*` field to be `null`. **Agricultural monitoring.** Soil variables are hourly only. ```json { "locations": ["47.05,8.31", "45.42,11.87"], "variables": ["soil_temperature_0cm", "soil_moisture_0_to_7cm", "precipitation"], "dailyVariables": ["precipitation_sum", "et0_fao_evapotranspiration"], "forecastDays": 10, "timezone": "auto" } ``` Run it daily and append; `sourceUrl` on each row records exactly what was asked for, so an odd series can be reproduced later.