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.
-
Confirm the model declaration. Read Choose models and Model capabilities. The slug in
models.jsonis its identity. -
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 -
Use a short smoke cap when appropriate.
--max-steps 2selects the first two scheduled steps. It limits forecast steps, not sites or fields. -
Run the build. Remove
--dry-run; remove the cap for the full declared horizon. -
Inspect output before deploying it. Confirm the model manifest and each expected site profile validate and share the same
referenceTime.
Complete command surface
Section titled “Complete command surface”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:
mkdir -p public/data/hrdps-continental/sitescurl -sS -o public/data/hrdps-continental/manifest.json \ https://data.meteo.azohra.com/hrdps-continental/manifest.jsoncurl -sS -o public/data/hrdps-continental/sites/dundee.json \ https://data.meteo.azohra.com/hrdps-continental/sites/dundee.jsonpnpm add windgramThen 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.