GEFS ensemble exceedance: how many members cross your threshold?
Averaging 31 GEFS members destroys a threshold signal weeks ahead. One paid call counts them member by member and returns a counted fraction per day, with the member count behind it.
By Matthias Begot · · Updated
An agent that has to commit weeks ahead — a parametric payout trigger, a
shipping window, a harvest slot, a concrete pour — asks a threshold question:
will this day see at least 30 mm of rain? Past roughly two weeks, the usual
answer is an ensemble mean, and averaging is exactly the operation that
destroys a threshold signal. One paid call —
GET /weather/forecast/exceedance —
counts the NOAA GEFS members one by one, before any averaging, and returns
one row per UTC calendar day: members crossing, members counted, and the
fraction of the two. The whole series settles as one x402 payment.
The number it returns is a counted fraction, not a calibrated probability. That distinction is the substance of this article, and the endpoint states it in its own response.
The problem: an average cannot answer a threshold question
Past the deterministic window (about 384 h, or J+16),
GET /weather/forecast and
GET /weather/forecast/daily serve the
average of ~31 ensemble members. That is what an ensemble mean is, not a
defect: ECMWF’s Forecast User Guide states plainly that the mean “smooths the
flow more in areas of large uncertainty (large spread)” and that with large
spread it “can be a rather weak pattern and may not represent any of the
possible states”
(ECMWF FUG §8.1.2).
An extreme produced by a subset of members survives the average as a fraction of
itself.
Measured in production on 2026-07-25 for Paris, on
/weather/forecast/daily?lat=48.85&lon=2.35&days=35:
| Segment | Register | Daily precipitation | Daily max temperature |
|---|---|---|---|
| J+0 → J+15 (16 days) | deterministic GFS | 9 days at 0.0 mm, peak 13.12 mm | 21.2 → 40.5 °C, a 19.3 °C spread |
| J+16 → J+34 (19 days) | GEFS ensemble mean | no day below 0.40 mm, none above 1.82 mm | 25.3 → 27.9 °C, a 2.6 °C spread |
Read the second row as an agent would: a rule like “more than 30 mm in a day” placed on that tail will essentially never fire, whatever weather actually occurs. Nineteen days of a 35-day series collapse into a band 1.4 mm wide. An agent polling the mean and comparing it to a contractual threshold is not getting a cautious answer — it is getting a structurally dead one.
What counting the members instead looks like
The endpoint reads the member grids rather than their mean, reduces each member to one value per day, and compares that value to your threshold. Three parameters carry the question:
| Parameter | Role |
|---|---|
variable | The quantity tested: precipitation (mm), wind (m/s) or temperature (°C) |
threshold | The threshold value, in that variable’s unit — echoed back as threshold_unit |
comparison | at_least (default) or at_most; both bounds inclusive |
variable and threshold are required together: a threshold without a
quantity has no unit, and the unit applied is never implicit. The point is
located either by coordinates or by place name (city=Manila, narrowed by
country=), the same gazetteer contract as the other forecast routes.
GET /weather/forecast/exceedance?city=Manila&variable=precipitation&threshold=30&days=3
{
"date": "2026-08-25",
"statistic": "daily_sum",
"members_total": 31,
"members_exceeding": 9,
"probability": 0.2903,
"steps_used": 4,
"coverage": { "complete": true }
}
Captured in production on 2026-08-09. Read that row as “9 of the 31 members put at least 30 mm of rain on 2026-08-25”. Not “a 29 % chance of rain”.
The honesty line: a counted fraction is not a probability
This is the part an agent must not paper over. 0.2903 is
members_exceeding / members_total and nothing else. No model output
statistics, no bias correction against observations, no reliability calibration
has been applied. Turning a raw ensemble frequency into a calibrated probability
is a whole discipline — the statistical literature on tail calibration exists
precisely because raw ensemble frequencies are poorly behaved at high thresholds
(Allen et al., Tail calibration of probabilistic forecasts).
This endpoint does not pretend to have done that work.
Two design consequences follow, and both are worth copying in any system that serves derived numbers:
members_totalis served on every single day, never assumed equal to the nominal 31. The fraction can be divided back out and audited. A0.5counted on 4 members and a0.5counted on 31 are not the same statement, and the response hands you both numbers so you can tell them apart.- The caveat travels with the number. Every response carries a
methodobject withkind: "counted_member_fraction",calibrated: falseand a note saying so in words. An agent that never reads a documentation page still receives the caveat attached to the value it is about to act on.
probability is null, not 0, when no member carried a value for that day —
an empty sample has no fraction, and none is invented.
The reduction is named, and the direction flips it
Each member is reduced to one value per day before the threshold test, and the
response names which reduction was applied in statistic:
variable | comparison | statistic | Reduction |
|---|---|---|---|
precipitation | either | daily_sum | Sum over the day, rebuilt per member from disjoint accumulation buckets |
wind | either | daily_max | Largest 10 m scalar speed sqrt(u² + v²), per member — never a vector mean of components |
temperature | at_least | daily_max | Warmest value of the day |
temperature | at_most | daily_min | Coldest value of the day |
That last row is the one that saves a real trigger. A frost threshold
(variable=temperature&threshold=0&comparison=at_most) tested against the daily
maximum would never fire; the direction of the comparison flips the reduction
to daily_min. And a 0.0 on such a query is a counted zero on 31 members
— an honest answer, not a missing one.
Degraded days are served and marked, never dropped
Ensemble tails are ragged at the edges. Four failure modes can appear in a day’s
coverage.reason, and none of them removes the day from the series:
- Clipped by the window start or end — the first day of the tail usually begins mid-day; the day is served with the instant it is covered from.
- Below the member floor —
counted on N of 31 ensemble members. Under the 21-member floor the day is still served with its realmembers_total, and nothing is extrapolated back up to 31. - Members with missing steps — such a member is counted, not dropped. Dropping it would silently shrink the denominator and inflate the fraction of the members that remain.
Days that fall outside the covered window are simply omitted, never an error.
Only a window lying entirely outside the ensemble tail is a 400 OUT_OF_RANGE —
and that error names the deterministic routes to use instead, so an agent that
aimed at the wrong horizon finds its way back without a human.
Per the x402 golden rule, a well-formed request returning at least one counted
day is a successful answer — 200 — even when an edge day is clipped: that is
reported through coverage, never hidden. And when a deployment holds no member
grids at all, the response is a 200 with an empty series flagged as carrying no
usable result, which the gateway reads as a signal not to settle: the agent
asked nothing wrong and is not charged for an empty answer.
Scope, stated plainly
- It is a grid cell, not a weather station. The member grids are a ~0.5°
mesh;
grid.distance_kmexposes how far the served cell sits from your point. - It is the tail, not the near term. The window is capped at 20 days, the physical span of the tail the members cover. Inside the deterministic window, this is the wrong route and it says so.
- The member layer runs one cycle behind. GEFS runs 4 cycles a day out to 16
days, except 35 days at 00 UTC — that extension publishes the following day,
so a healthy response here is normally
cached, notlive(NOAA EMC, GEFS v12). - Omit
daysand the default window is budget-capped, flagged withwindow_capped: true; askdays=explicitly and it is served in full.
Where it fits in the x402 loop
The call follows the same pattern as every Invoket endpoint: discover, take the
402, pay, replay. The Quickstart walks that cycle with
runnable snippets and For agents covers the discovery
surfaces. The whole day series — however many days it counts — settles as one
payment for the call. The price is served live by the
catalog and is not pinned in this article.
Among the forecast routes, pick by the shape of the question:
GET /weather/forecast— one lead time at one point, deterministic within 16 days.GET /weather/forecast/daily— the daily series over the same range, in one call.GET /weather/forecast/exceedance— a counted threshold fraction over the ensemble tail, weeks out.GET /climate/indices— the same threshold grammar applied to the past, on ERA5, for parametric triggers that need a measured index rather than a forecast one.
A trigger agent usually needs two of these: the historical index that defines
the contract, and the counted fraction that says how close the ensemble is to
crossing it. Used for what it is — a raw counted member fraction, with the
denominator exposed and the caveat attached — the exceedance route gives an
agent something it can act on weeks ahead without inventing a probability nobody
computed. For the full parameter list, coverage rules and error codes, see the
GET /weather/forecast/exceedance documentation.
GEFS data comes from NOAA and is public-domain U.S. government data.