The auction is public
Day-ahead prices for the next twenty four hours are published every afternoon, free, for anyone who goes and collects them. So is the renewable-share forecast.
whenrun · slot 03 · shipped
Watches day-ahead electricity prices and the renewable-share forecast, works out the cheapest or greenest window that still meets your deadline, and starts the job there.
today ▄ 176 EUR/MWh (1h left)
tomorrow ▆▅▅▅▅▅▇█▇▆▅▃▂▁▁▃▅▇██▇▅▅▄ 72 to 231 EUR/MWh
00:00 06:00 12:00 18:00
len when spot you pay
1h tomorrow 13:15 to 14:15 68.3 EUR/MWh 31.3 ct/kWh delivered
4h tomorrow 11:30 to 15:30 86.6 EUR/MWh 33.5 ct/kWh delivered
Greenest 2h tomorrow 12:30 to 14:30 101% renewable
The situation
Power can cost three times more in the evening than it does at midday, and your backup job does not care when it runs.
Day-ahead prices for the next twenty four hours are published every afternoon, free, for anyone who goes and collects them. So is the renewable-share forecast.
Since January 2025 every German electricity supplier has had to offer a dynamic tariff. The price you pay can follow the auction if you ask it to.
Something that acts on both without tying you to one supplier's app. That is the whole of this tool, and it is the first one on this shelf that acts rather than reports.
Running it
A fresh clone with Node 22 on it does everything below. Storage is a file on disk. No account, no key, no build step.
git clone https://github.com/gledach/whenrun && cd whenrun
node bin/whenrun.mjs prices # what power costs today
node bin/whenrun.mjs where # your location, and every zone
node bin/whenrun.mjs doctor # config, connectivity, data freshness
It ships set to postcode 82319 Starnberg in bidding zone DE-LU.
where prints that and every zone it supports, and
cp config/location.default.mjs config/location.local.mjs changes
it for good. The local copy is gitignored, which is the right place for your
postcode if you would rather it were not in a published repository.
| Command | What it does |
|---|---|
| gledach-whenrun prices | Today and tomorrow, with the cheap windows marked. |
| gledach-whenrun plan | When should this run, and what will it cost. Takes --duration, --kw, --by and --objective, or runs every task in your config. |
| gledach-whenrun run --task=backup | A dry run. Add --execute and it waits for the window, then starts the job. |
| gledach-whenrun daemon --execute | All enabled tasks, continuously. --once makes it a single pass for cron or Task Scheduler. |
| gledach-whenrun report | What it actually saved, against the delivered bill rather than the spot price. |
| gledach-whenrun where | Your location and every bidding zone it knows. |
| gledach-whenrun doctor | Config, connectivity and data freshness. Warns while you are still on the shipped tariff defaults. |
The command is gledach-whenrun. npm link puts that name
on your PATH. The copy block above calls the same file and needs no link. Nothing
named whenrun is installed.
Three objectives: cheapest minimises price, greenest
maximises renewable share, balanced blends the two. A job marked
interruptible may be scattered across non-adjacent slots.
Otherwise it gets one unbroken block. Your task is told what it was scheduled
into through the environment: WHENRUN_WINDOW_START,
WHENRUN_WINDOW_END, WHENRUN_SPOT_EUR_MWH,
WHENRUN_RENEWABLE_PCT and WHENRUN_SLOTS.
Safety
Which is a thing worth being careful about rather than quiet about. Four rules it holds to, all of them mechanical.
Never from a price feed, never from an argument, never from the network. There is no code path that builds a command out of fetched data.
A task needs enabled: true and the command needs
--execute. A dry run is the default everywhere.
Stale price data, an impossible deadline, or a window that closed while the daemon was asleep each produce a refusal with a reason, not a best effort.
An agent can ask when power is cheap. It cannot start anything.
What the savings number means
A German bill is roughly spot plus supplier markup plus grid fees plus levies plus taxes plus VAT, and only the first of those moves by the hour. So if spot halves, your bill does not.
saving 0.04 EUR (31.5% of this job's bill) 11% of its energy component
Both numbers, both labelled. The flattering one is always the figure marked
as the energy component. report also prints your standing charge
for the period, because no amount of load shifting changes it and a savings
figure without that context flatters the tool.
Where the numbers come from
doctor cross-checks the two against each other and warns when
they disagree by more than a rounding error.
| Source | Zones | Key | Resolution |
|---|---|---|---|
| Energy-ChartsFraunhofer ISE | DE-LU, AT, FR, NL and more | none | 15 min |
| aWATTar | DE, AT | none | 60 min |
Price data is CC BY 4.0 from Bundesnetzagentur and SMARD.de by way of Energy-Charts. Verified against live data for DE-LU, AT, FR, CH, NL, ES and PL. A bidding zone is not a country: Germany and Luxembourg share DE-LU, Denmark is split at the Great Belt, and Italy and Norway have several. The renewable-share forecast is published per country and derived from the zone, so you only set one thing.
For agents
Every result carries a coverage block with a freshness status and a
trustEmptyResult flag, so an agent can tell prices are
flat
from I have not been able to look since Tuesday
.
node mcp-server.mjs
get_prices · cheapest_window · greenest_window
list_tasks · plan_task · savings_report
What it cannot see
Every tool on this shelf ships a written audit of its own blind spots. This is whenrun's, in full.
Your actual tariff. It uses the numbers in your
tariff file. If those are wrong, every euro it reports is wrong by the same
proportion. doctor warns while you are still on the shipped
defaults.
Whether you are on a dynamic tariff at all. On a fixed tariff it will still tell you when power is cheap, and you will still be charged the same.
Your own generation. Solar on your roof changes the answer completely and is not modelled.
Grid fees that vary by time. Modelled as flat. Where a time-varying network tariff applies, the cheap window may not be exactly where this says it is.
Whether your job really draws what you declared.
kw is your estimate, not a measurement.
It cannot pause a running command.
interruptible shapes which slots are chosen and passes the list
to your task in WHENRUN_SLOTS. Honouring the split is your
script's job.
Tests: 128, fully offline, no network. A separate live suite checks that the real APIs still answer in the expected shape, and refreshes the captured fixtures. The offline fixtures are captured from the live APIs rather than handwritten, so they test what the API does rather than what someone remembered it doing.
Elsewhere on the shelf
Each takes whatever shape suits it, from a single command to a dashboard. What they share is the promise rather than the plumbing: free, installable, and honest about what each one cannot see.
Slot 01 · live preview
Ten watchers and one inbox on whichever market you name, scored and attributed, answerable by an agent over MCP.
Open the page →
Slot 04 · shipped
Which domains every device on your network contacts, and when one starts talking somewhere new.
Open the page →
Slot 05 · shipped
Four public registers checked against a list of the things you own, with every match graded.
Open the page →
Slot 06 · shipped
Which API hosts your local coding agents connect to, and whether one of them is leaking sockets.
Open the page →
Slot 07 · shipped
Remembers the hooks a coding agent runs at startup, and the editor tasks that run when a folder opens. Prints only when that set changes. It does not delete a hook.
Open the page →
Slot 08 · shipped
Takes a snapshot before an agent session and another after, then shows what changed outside that repo: files, startup entries, services, packages and ports. It does not delete, kill or block anything.
Open the page →
Slot 02 · reference
What is watchable, what it costs, and eight engineering decisions each extracted from a bug that shipped.
Open the page →