Skip to content

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

FactValue
Published at<model>/sites/<site>.json; each history archive line is the same document
ParseparseSiteForecastJson (raw text) / parseSiteForecast (parsed value) from @azohra/meteo.briefing/contract
Zod authoritysiteForecastSchema, pinning SITE_FORECAST_SCHEMA_VERSION (2)
JSON Schemaprofile.schema.json
Discoverythe model catalogue’s models array names every model that publishes profiles

Hour blocks

BlockRepresentative valuesContract rule
surfacepressure, temperature, moisture, winds, heat fluxes, precipitation, and declared optional science fieldsA model that lacks an optional field leaves it out; it is not filled with zero
levelsheight, temperature, dew point, wind, and optional omega and cloud fractionEntries ascend by height; pressure is the isobaric coordinate
derivedboundary-layer top, thermal velocity, cloud base, usable-lift topThe 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.

read-wind.ts
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.