--- name: uk-carbon-intensity-scraper description: Read half-hourly carbon intensity for the Great Britain electricity grid via the Apify Actor arman-bd/uk-carbon-intensity-scraper. Returns one record per settlement period with forecast and settled actual intensity in gCO2/kWh, the intensity band, and the generation mix by fuel both as an array and as flat per-fuel columns, nationally or for any of the eighteen grid regions or a postcode. Use when a task needs carbon-aware scheduling, Scope 2 reporting, load shifting, renewables share analysis or regional grid comparison. Not for electricity prices, demand or capacity figures, other countries, or Northern Ireland. --- # UK Carbon Intensity Scraper Apify Actor `arman-bd/uk-carbon-intensity-scraper`. One dataset record per half-hourly settlement period, nationally or per region. It takes no credentials of your own. Four modes cover now, the next 48 hours, a historical window and the regional breakdown. ## When to use it - Carbon-aware compute or batch scheduling, from the 48-hour forecast. - Scope 2 and ESG reporting: a full year of settled intensity multiplied by metered consumption. - Smart charging or heat-pump control for a specific postcode. - Tracking wind and solar share over time from the generation mix. - Comparing regions, which can differ by an order of magnitude at the same instant. ## When not to use it - Wholesale prices, demand, capacity or balancing data. Only intensity and fuel mix are returned. - Any grid outside Great Britain. Northern Ireland runs on a separate system and is not covered. - Per-site or per-meter consumption. You supply that side yourself and multiply. - Data before May 2017, which is when the published series begins. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/uk-carbon-intensity-scraper').call({ mode: 'historical', fromDate: '2026-07-01', toDate: '2026-08-01', includeGenerationMix: true, }); 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~uk-carbon-intensity-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mode":"historical","fromDate":"2026-08-01","toDate":"2026-08-02"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/uk-carbon-intensity-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `mode` | string | no | `"current"` | `current` for the settlement period happening now, nationally. `forecast` for the next 48 hours, nationally. `historical` for a national window. `regional` for the per-region breakdown. Case-insensitive; anything else throws. | | `fromDate` | string | no | none | `YYYY-MM-DD`, an ISO 8601 timestamp, or a millisecond epoch. **Required for `historical`.** In `regional` mode it turns a snapshot into a time series. In `forecast` mode it anchors the 48-hour horizon. Ignored in `current`. Unparseable values throw. | | `toDate` | string | no | now | Same formats. Must be after `fromDate`. Windows longer than 14 days are split into 14-day requests automatically. | | `regions` | string[] | no | `[]` | Region IDs `1` to `18`, or UK postcodes. Full postcodes are trimmed to the outward code, so `SW1A 1AA` and `SW1A` behave identically. Unusable entries are dropped and listed in the summary. Ignored outside `regional` mode; empty in `regional` mode returns all eighteen in one request. | | `includeGenerationMix` | boolean | no | `true` | Include the per-fuel shares. Off suppresses them even on regional responses, which carry a mix regardless. | | `maxResults` | integer | no | `0` | Cap on total readings saved across every request. `0` or negative means no limit. A year of half-hourly national data is about 17,500 readings. | **The mode decides which of the other fields matter and which fields come back populated.** `current` and `forecast` ignore `fromDate` for windowing and ignore `regions` entirely. `historical` refuses to run without `fromDate`. `regional` is the only mode that reads `regions`, and it changes shape depending on whether `fromDate` is present: without it you get one current reading per target, with it you get a time series per target, and each target and each 14-day segment is its own request. So `regional` over four postcodes for a year is roughly 108 requests, while the same year nationally is 27. ## Output One record per settlement period, per region where applicable. | Field | Type | Notes | |---|---|---| | `from` | string \| null | Start of the half-hour settlement period, in the source's short ISO form such as `2026-08-06T11:00Z`. Note there are no seconds. | | `to` | string \| null | End of the same period, same format. | | `intensityForecast` | number \| null | Forecast intensity in gCO2/kWh. | | `intensityActual` | number \| null | Settled actual intensity. **National and past only**: `null` for every regional reading and for every future period. | | `intensityIndex` | string \| null | Band: `very low`, `low`, `moderate`, `high` or `very high`. | | `region` | number \| null | Region ID 1 to 18, or `null` for a national reading. | | `regionName` | string \| null | `National`, or the region short name such as `London`. | | `generationMix` | object[] \| null | The source array: `[{ fuel, perc }, …]`. `null` when suppressed or unavailable. | | `fuelShares` | object \| null | The same numbers flattened to `{ biomass: 7.9, coal: 0, … }`, so they survive a CSV export. `null` whenever `generationMix` is. | | `postcode` | string \| null | The outward code you queried, when the query was by postcode. `null` otherwise. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A national record: ```json { "from": "2026-08-06T11:00Z", "to": "2026-08-06T11:30Z", "intensityForecast": 62, "intensityActual": 61, "intensityIndex": "low", "region": null, "regionName": "National", "generationMix": [ { "fuel": "biomass", "perc": 7.9 }, { "fuel": "coal", "perc": 0 }, { "fuel": "imports", "perc": 17.2 }, { "fuel": "gas", "perc": 7.5 }, { "fuel": "nuclear", "perc": 12.7 }, { "fuel": "other", "perc": 0 }, { "fuel": "hydro", "perc": 0 }, { "fuel": "solar", "perc": 24.5 }, { "fuel": "wind", "perc": 30.2 } ], "fuelShares": { "biomass": 7.9, "coal": 0, "imports": 17.2, "gas": 7.5, "nuclear": 12.7, "other": 0, "hydro": 0, "solar": 24.5, "wind": 30.2 }, "postcode": null, "scrapedAt": "2026-08-06T12:00:00.000Z" } ``` ## 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 { "mode": "regional", "requestsQueued": 4, "requestsMade": 4, "sourcesFailed": 1, "failures": [ { "source": "postcode RG10 2026-08-01T00:00Z → 2026-08-08T00:00Z", "error": "rejected by API (Invalid postcode)" } ], "regionsRejected": ["not a postcode"], "readingsSaved": 1008, "filters": { "fromDate": "2026-08-01T00:00Z", "toDate": "2026-08-08T00:00Z", "regions": ["SW1A 1AA", "RG10"], "includeGenerationMix": true, "maxResults": 0 }, "finishedAt": "2026-08-06T12:00:09.221Z" } ``` Three separate things can shrink a result and they are reported separately. `regionsRejected` lists inputs that never became a request at all, usually a malformed postcode. `failures` lists requests that were made and failed, each labelled with its target and window, so a gap in a time series can be traced to one 14-day segment. `requestsMade` below `requestsQueued` means the run stopped early on `maxResults`, so the tail of the window is missing rather than empty. `filters.toDate` is `null` whenever no `fromDate` was given, since there is no window in that case. ## Behaviour to plan around - **`intensityActual` is national and settled only.** Every regional record has it as `null`, because regional figures are forecast-based, and every future period has it as `null` because it has not settled. The field is always present so your schema stays stable. Fall back to `intensityForecast` rather than treating `null` as zero. - **Forecast mode has no generation mix.** The mix series has no forward window, so `generationMix` and `fuelShares` are `null` throughout a `forecast` run. Regional forecasts do carry a mix, because it is embedded in the regional response. - **The national mix is joined on `from`.** It comes from a second request per window, keyed on the same settlement periods. If that request fails, the intensity records are still saved with `generationMix: null`, and only a warning is logged. - **`fuelShares` is the field to use in a table.** `generationMix` is the source array and flattens badly into CSV or a spreadsheet; `fuelShares` is the same numbers as named columns. Both are `null` together. - **Long windows are split into 14-day requests automatically.** A year is 27 sequential requests nationally, and 27 per target in regional mode. Each is independent, so one failed segment leaves a hole rather than killing the run. - **Timestamps have no seconds field.** `from` and `to` come back as `2026-08-06T11:00Z`, which some date parsers reject. Pad the seconds before parsing. - **Window bounds are floored to the containing half hour,** so an arbitrary start time snaps back to the settlement period it falls inside. - **Region IDs run 1 to 18.** 1 to 14 are the distribution regions roughly north to south, then 15 England, 16 Scotland, 17 Wales, 18 the whole of GB. Run `regional` with an empty `regions` list once to see all of them with their names. - **A postcode is trimmed to its outward code** before use, so `M1 1AE` becomes `M1`. Do not expect street-level resolution: this is a regional figure. - **`maxResults` stops the run mid-plan.** Remaining requests are never issued, which is visible as `requestsMade` below `requestsQueued`. Raise the cap rather than assuming the later windows were empty. - **Transient failures are retried** four times with exponential backoff. A rejected region or a malformed window fails immediately, with the source's own message copied into `failures`. - **One failed request never aborts the run.** The Actor only throws when every request it made failed. ## Recipes **Carbon-aware scheduling.** The next 48 hours, nationally. ```json { "mode": "forecast" } ``` Take the minimum `intensityForecast` across the returned periods and schedule into that window. Expect `generationMix` to be `null` on every row. **Scope 2 reporting for a year.** Settled national data, mix off to keep the dataset small. ```json { "mode": "historical", "fromDate": "2025-01-01", "toDate": "2026-01-01", "includeGenerationMix": false } ``` Multiply `intensityActual` by your half-hourly metered consumption. Check `sourcesFailed` first: a hole in a year is one missing 14-day segment. **Regional load shifting.** Two postcodes over a week. ```json { "mode": "regional", "regions": ["SW1A 1AA", "RG10"], "fromDate": "2026-08-01", "toDate": "2026-08-08" } ``` Group by `postcode` and rank periods by `intensityForecast`; `intensityActual` will be `null` throughout because these are regional readings. **Snapshot of the whole country.** All eighteen regions, one request, right now. ```json { "mode": "regional", "regions": [] } ``` One row per region for the current settlement period, each with its own `fuelShares`. Good for a dashboard tile or a regional comparison. **Half-hourly national poll.** Cheapest possible scheduled run. ```json { "mode": "current" } ``` One record per run. Append them to build your own series, keyed on `from`.