Skip to content

Run one model

The supported publisher interface is windgram build. Model-specific Python modules remain implementation entry points; downstream automation should use the CLI so external site and output paths are scoped consistently.

  1. Confirm the model declaration. Read Choose models and Model capabilities. The slug in models.json is its identity.

  2. Preflight paths and selection. This performs no network access or writes:

    Terminal
    uv run --project pipeline windgram build --model hrrr-conus --sites ./club-sites.json --output ./public/data --dry-run
  3. Use a short smoke cap when appropriate. --max-steps 2 selects the first two scheduled steps. It limits forecast steps, not sites or fields.

  4. Run the build. Remove --dry-run; remove the cap for the full declared horizon.

  5. Inspect output before deploying it. Confirm the model manifest and each expected site profile validate and share the same referenceTime.

windgram build (--model SLUG | --all)
[--sites PATH]
[--output PATH]
[--max-steps N]
[--dry-run]

Defaults are ./sites.json and ./data. --model and --all are mutually exclusive. Unknown model slugs, missing or unreadable site files, invalid site catalogues, and unusable output paths fail with an actionable error.

Each builder detects the latest complete provider run and exits without rewriting output when that referenceTime is already published. “Already published” is read from the public dataset over HTTPS — WINDGRAM_DATA_BASE selects the data root (default https://data.meteo.azohra.com), so a publisher maintaining its own tree points that variable at its own root; the same base seeds the month archives that history appends continue. A successful new run writes current profiles, appends history, and writes the model manifest. Schedule builds applies the catalogued publication cadence to recurring jobs.

Render a published profile into a club page

Section titled “Render a published profile into a club page”

Everything below runs against the real published dataset — the same documents any consumer fetches. Mirror one model’s manifest and one launch profile into your project’s static tree:

Terminal
mkdir -p public/data/hrdps-continental/sites
curl -sS -o public/data/hrdps-continental/manifest.json \
https://data.meteo.azohra.com/hrdps-continental/manifest.json
curl -sS -o public/data/hrdps-continental/sites/dundee.json \
https://data.meteo.azohra.com/hrdps-continental/sites/dundee.json
pnpm add windgram

Then render.mjs is the whole publisher: validate both documents against the contract, confirm they describe the same model run, build the scene in the launch’s own declared timezone, and write a self-contained page.

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { parseWindgramManifestJson, parseWindgramProfileJson } from "windgram/contract";
import { buildKeySpec, buildScene } from "windgram/scene";
import { renderKeySvg, renderSvg } from "windgram/svg";
import { runsConsistent } from "windgram/transport";
const model = "hrdps-continental";
const launch = "dundee";
const profile = parseWindgramProfileJson(
await readFile(`public/data/${model}/sites/${launch}.json`, "utf8"),
);
if (!profile) throw new Error("profile failed the windgram contract");
const manifest = parseWindgramManifestJson(
await readFile(`public/data/${model}/manifest.json`, "utf8"),
);
if (!manifest) throw new Error("manifest failed the windgram contract");
if (!runsConsistent(manifest, profile)) {
throw new Error("manifest and profile do not describe the same model run");
}
const timeZone = profile.site.timeZone;
if (!timeZone) throw new Error("profile does not declare the launch timezone");
const scene = buildScene(profile, { timeZone, widthPx: 1080 });
await mkdir("public/assets", { recursive: true });
await writeFile(`public/assets/${profile.site.id}.svg`, `${renderSvg(scene)}\n`);
await writeFile(
`public/assets/${profile.site.id}-key.svg`,
`${renderKeySvg(buildKeySpec(scene))}\n`,
);
const page = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${profile.site.name} — windgram</title>
<style>
body { max-width: 78rem; margin: 0 auto; padding: 2rem; font-family: system-ui, sans-serif; }
img { display: block; width: 100%; height: auto; margin-block: 1.5rem; }
</style>
</head>
<body>
<h1>${profile.site.name}</h1>
<img src="assets/${profile.site.id}.svg" alt="Windgram for ${profile.site.name}">
<img src="assets/${profile.site.id}-key.svg" alt="Windgram key">
<p>
<a href="data/${model}/sites/${launch}.json">Profile JSON</a> ·
<a href="data/${model}/manifest.json">Manifest JSON</a>
</p>
</body>
</html>
`;
await writeFile("public/index.html", page);

node render.mjs leaves public/ as a complete static tree — page, chart, scene-derived key, and the inspectable contract documents it was rendered from. Copy it to any static host; your club supplies the access, presentation, and retention policy. Rerunning the fetch and render publishes a newer run; to publish your own launches instead, run the builder CLI with your catalogue and point the fetch at your own output.

analyzeProfile() from windgram/analyze can turn the validated profile into typed, evidence-carrying findings. Those findings are not publisher artifacts: their thresholds are reader conventions, and a publisher must supply their context and limitations.

The SVG guide defines scene-derived key semantics.