Profile document
A profile is the document the forecast engine publishes for one site and one model run, and the one the briefing package reads and draws. It records its contract version, model, publication, site, optional provider semantics, and every forecast hour in time order. The consumer chooses day windows and local time. New documents carry the site’s timezone to help with that.
profile├── schemaVersion + model├── run { referenceTime, generatedAt, members? }├── site { identity, coordinates, model elevation, timeZone? }├── semantics? { gust?, precipitation?, smoke? }└── hours[] ├── validAt ├── surface ├── levels[] ├── derived └── smoke? { surfaceUgm3, columnMgm2, aot }{ "schemaVersion": 2, "model": "hrdps-continental", "run": { "referenceTime": "2026-08-12T12:00:00Z", "generatedAt": "2026-08-12T18:22:29Z" }, "site": { "id": "test-hill", "name": "Test Hill", "latitude": 49.4581, "longitude": -117.3956, "modelElevationM": 1180.8, "timeZone": "America/Vancouver" }, "semantics": { "gust": "hourMax", "precipitation": "windowMeanRate" }, "hours": [ { "validAt": "2026-08-12T20:00:00Z", "surface": { "temperatureC": 20.27, "windSpeedMps": 1.18, "windGustMps": 2.29, "sensibleHeatFluxWm2": 224.4, "capeJkg": 1045, "pblHeightM": 1274, … }, "levels": [ { "pressureHpa": 875, "heightM": 1253.8, "temperatureC": 17.99, "dewPointC": 9.06, "windSpeedMps": 1.87, "windDirectionDeg": 5 }, … ], "derived": { "boundaryLayerTopM": 2136.9, "thermalVelocityMps": 1.8, "cloudBaseM": 2362.8, "usableLiftTopM": 2362.8 } }, … ]}Abbreviated from hrdps-continental/sites/test-hill.json in the live sample
dataset. The values are real and come from the peak-w* hour; … marks
elided fields, levels, and hours.
On the wire
| Fact | Value |
|---|---|
| Published at | <model>/sites/<site>.json; each history archive line is the same document |
| Parse | parseSiteForecastJson (raw text) / parseSiteForecast (parsed value) from @azohra/meteo.briefing/contract |
| Zod authority | siteForecastSchema, pinning SITE_FORECAST_SCHEMA_VERSION (2) |
| JSON Schema | profile.schema.json |
| Discovery | the model catalogue’s models array names every model that publishes profiles |
Hour blocks
| Block | Representative values | Contract rule |
|---|---|---|
surface | pressure, temperature, moisture, winds, heat fluxes, precipitation, and declared optional science fields | A model that lacks an optional field leaves it out; it is not filled with zero |
levels | height, temperature, dew point, wind, and optional omega and cloud fraction | Entries ascend by height; pressure is the isobaric coordinate |
derived | boundary-layer top, thermal velocity, cloud base, usable-lift top | The forecast engine computes these from the full set of required inputs |
Run, site, and semantics
run.referenceTime is the model’s initialization time, and
run.generatedAt identifies this publication of it. Ensemble documents also
state their total membership once, at run.members.
The site block records where the atmosphere was sampled (id, name,
latitude, longitude, and an optional timeZone) and the ground height the
model assumes there (site.modelElevationM). That height is the chart’s floor
and the reference for the physics.
The block has no launch elevation, because a launch stored in the document
would tie a grid forecast to one launch without saying so. The measured launch
elevation is the elevation pick in
site-context.json. Consumers pass it
at render time as MeteogramOptions.launch, so one document serves every
launch its grid cell covers. Older stored documents may still carry an
altitudeM field, which current parsers ignore.
The optional site.timeZone is copied from the IANA zone that the site
catalogue requires, so a new document carries its own local-time context and
needs no catalogue lookup. The field is optional because older stored profiles
predate it. A missing zone means the document is older; it does not mean the
launch uses UTC. The caller then has to choose a timezone. The
contract guide lists
what each API does in that case.
The optional semantics block keeps the meaning of gust, precipitation, and
smoke values with a stored document, so it can be read without the model
catalogue. When the block is absent, no default applies. Where a model
declares smoke (radiativelyCoupled or passive), each hour may also carry an
optional smoke block with surfaceUgm3, columnMgm2, and aot, all three
required when the block is present. The
smoke document reference explains them.
Deterministic and ensemble values
Any numeric position can hold either a number or an ensemble percentile object. Consumers tell them apart by the value’s shape, without checking the model name.
import { isEnsembleDropout, isEnsembleValue, type SiteForecast } from "@azohra/meteo.briefing/contract";
export function firstWindSpeed(profile: SiteForecast): number | null | undefined { const speed = profile.hours[0]?.surface.windSpeedMps;
return speed === undefined ? undefined : isEnsembleValue(speed) ? isEnsembleDropout(speed) ? null : speed.p50 : speed;}Ensemble values explains per-position contributor counts and censoring.
Validate before use
Use parseSiteForecastJson for raw stored text and parseSiteForecast for
a value you have already parsed. Both return null when the document fails
validation. The package schemas and the generated JSON Schema define every
field constraint.
Discover capabilities
models.json is the machine-readable catalogue. It declares which models
exist, their cadence and levels, and what their optional capabilities mean.
Render from those declarations; models do not all supply the same fields.
The contract guide’s unit boundary map
maps units across the integration points. The full field-by-field contract and
the generated JSON Schema are in the
@azohra/meteo.briefing package.