# Event Wire Contract This document is the canonical JSON contract for events emitted by `weatherfeeder` JSON sinks, including stdout and NATS. Postgres stores the same event envelope fields in parent table columns; see [Postgres integration](postgres.md). Downstream consumers should read the envelope, switch on `schema`, and decode `payload` according to that schema. ## Envelope Every emitted event is a JSON object with these fields: | Field | Type | Required | Notes | |---|---|:---:|---| | `id` | string | yes | Stable event identifier. Treat as opaque. | | `kind` | string | yes | Routing kind, such as `observation` or `alert`. | | `source` | string | yes | Configured source name. | | `emitted_at` | timestamp | yes | When the daemon emitted the event. | | `effective_at` | timestamp | no | Timestamp the payload is about, when known. | | `schema` | string | no | Schema identifier. Weatherfeeder sources and normalizers set this. | | `payload` | object, array, string, or scalar | yes | Schema-specific payload. | Timestamps are JSON strings using Go `time.Time` JSON encoding, which is RFC3339Nano-compatible. Weatherfeeder normalizers use UTC timestamps for canonical payloads. ## Kinds And Schemas Canonical schemas emitted after normalization: | Kind | Schema | Payload | |---|---|---| | `observation` | `weather.observation.v1` | `WeatherObservation` | | `forecast` | `weather.forecast.v1` | `WeatherForecastRun` | | `forecast_discussion` | `weather.forecast_discussion.v1` | `WeatherForecastDiscussion` | | `weather_story` | `weather.weather_story.v1` | `WeatherStoryRun` | | `alert` | `weather.alert.v1` | `WeatherAlertRun` | | `outlook` | `weather.outlook.v2` | `WeatherOutlookRun` | Raw upstream schemas emitted by current sources: | Kind | Schema | Payload | |---|---|---| | `observation` | `raw.nws.observation.v1` | NWS observation JSON | | `observation` | `raw.openmeteo.current.v1` | Open-Meteo current JSON | | `observation` | `raw.openweather.current.v1` | OpenWeather current JSON | | `forecast` | `raw.nws.hourly.forecast.v1` | NWS hourly forecast JSON | | `forecast` | `raw.nws.narrative.forecast.v1` | NWS narrative forecast JSON | | `forecast_discussion` | `raw.nws.forecast_discussion.v1` | NWS forecast discussion HTML string | | `weather_story` | `raw.nws.weatherstories.v1` | NWS weather stories JSON | | `forecast` | `raw.openmeteo.hourly.forecast.v1` | Open-Meteo hourly forecast JSON | | `alert` | `raw.nws.alerts.v1` | NWS alerts JSON | | `outlook` | `raw.spc.convective_outlook.v1` | SPC convective outlook raw bundle | `standards.SchemaRawOpenWeatherHourlyForecastV1` exists in code, but no current registered source emits it. ## Shared Conventions - Canonical numeric measurements use metric units. - Floating-point values in canonical payloads are rounded to 4 digits after the decimal point during normalization. - Optional fields use JSON `omitempty`; absent fields should be treated as unknown. - `conditionCode` is a WMO weather interpretation code. Unknown observation conditions use `-1`. Forecast period `conditionCode` is optional. - Additive fields are compatible within a schema version. Removing, renaming, or changing the meaning of a field requires a new schema identifier. ## `weather.observation.v1` Payload type: `WeatherObservation`. | Field | Type | Required | Notes | |---|---|:---:|---| | `stationId` | string | no | Provider station/location identifier. | | `stationName` | string | no | Human station name. | | `timestamp` | timestamp | yes | Observation timestamp. | | `conditionCode` | integer | yes | WMO code; `-1` means unknown. | | `isDay` | boolean | no | Day/night hint. | | `textDescription` | string | no | Short human description. | | `temperatureC` | number | no | Celsius. | | `dewpointC` | number | no | Celsius. | | `windDirectionDegrees` | number | no | Degrees. | | `windSpeedKmh` | number | no | Kilometers per hour. | | `windGustKmh` | number | no | Kilometers per hour. | | `barometricPressurePa` | number | no | Pascals. | | `visibilityMeters` | number | no | Meters. | | `relativeHumidityPercent` | number | no | Percent from 0 to 100. | | `apparentTemperatureC` | number | no | Celsius. | | `presentWeather` | array | no | Provider-specific present weather fragments. | `presentWeather[]` entries contain optional `raw` objects. ## `weather.forecast.v1` Payload type: `WeatherForecastRun`. | Field | Type | Required | Notes | |---|---|:---:|---| | `locationId` | string | no | Provider location identifier. | | `locationName` | string | no | Human location name. | | `issuedAt` | timestamp | yes | When the forecast run was generated or issued. | | `updatedAt` | timestamp | no | Subsequent provider update time. | | `product` | string | yes | Current emitted values are `hourly` and `narrative`. | | `latitude` | number | no | Degrees. | | `longitude` | number | no | Degrees. | | `elevationMeters` | number | no | Meters. | | `periods` | array | yes | Ordered forecast periods. | `periods[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `startTime` | timestamp | yes | Period start. | | `endTime` | timestamp | yes | Period end. | | `name` | string | no | Human label. | | `isDay` | boolean | no | Day/night hint. | | `conditionCode` | integer | no | WMO code when applicable. | | `textDescription` | string | no | Human summary. | | `temperatureC` | number | no | Celsius. | | `temperatureCMin` | number | no | Celsius. | | `temperatureCMax` | number | no | Celsius. | | `dewpointC` | number | no | Celsius. | | `relativeHumidityPercent` | number | no | Percent from 0 to 100. | | `windDirectionDegrees` | number | no | Degrees. | | `windSpeedKmh` | number | no | Kilometers per hour. | | `windGustKmh` | number | no | Kilometers per hour. | | `barometricPressurePa` | number | no | Pascals. | | `visibilityMeters` | number | no | Meters. | | `apparentTemperatureC` | number | no | Celsius. | | `cloudCoverPercent` | number | no | Percent from 0 to 100. | | `probabilityOfPrecipitationPercent` | number | no | Percent from 0 to 100. | | `precipitationAmountMm` | number | no | Liquid-equivalent millimeters. | | `snowfallDepthMm` | number | no | Millimeters. | | `uvIndex` | number | no | Unitless index. | ## `weather.forecast_discussion.v1` Payload type: `WeatherForecastDiscussion`. | Field | Type | Required | Notes | |---|---|:---:|---| | `officeId` | string | no | NWS office identifier. | | `officeName` | string | no | Office name. | | `product` | string | yes | Current value is `afd`. | | `issuedAt` | timestamp | yes | Bulletin issue time. | | `updatedAt` | timestamp | no | Subsequent update time. | | `keyMessages` | array of strings | no | Extracted key messages. | | `shortTerm` | object | no | Short-term section. | | `longTerm` | object | no | Long-term section. | `shortTerm` and `longTerm` sections contain optional `qualifier`, `issuedAt`, and `text` fields. ## `weather.weather_story.v1` Payload type: `WeatherStoryRun`. | Field | Type | Required | Notes | |---|---|:---:|---| | `officeId` | string | no | NWS office identifier. | | `asOf` | timestamp | yes | Snapshot time. | | `stories` | array | yes | Ordered story cards. | `stories[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `officeId` | string | no | Office identifier. | | `startTime` | timestamp | yes | Story start. | | `endTime` | timestamp | yes | Story end. | | `updatedAt` | timestamp | yes | Story update time. | | `title` | string | no | Story title. | | `description` | string | no | Story description. | | `altText` | string | no | Image alternate text. | | `priority` | boolean | yes | Provider priority flag. | | `order` | integer | yes | Provider display order. | | `downloadUrl` | string | no | Story image URL. | ## `weather.alert.v1` Payload type: `WeatherAlertRun`. | Field | Type | Required | Notes | |---|---|:---:|---| | `locationId` | string | no | Provider location identifier. | | `locationName` | string | no | Human location name. | | `asOf` | timestamp | yes | Snapshot time. | | `latitude` | number | no | Degrees. | | `longitude` | number | no | Degrees. | | `alerts` | array | yes | Active alerts. | `alerts[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `id` | string | yes | Provider-stable alert identifier. | | `event` | string | no | Alert event label. | | `headline` | string | no | Alert headline. | | `severity` | string | no | Provider severity. | | `urgency` | string | no | Provider urgency. | | `certainty` | string | no | Provider certainty. | | `status` | string | no | Alert status. | | `messageType` | string | no | Alert message type. | | `category` | string | no | Alert category. | | `response` | string | no | Recommended response. | | `description` | string | no | Alert description. | | `instruction` | string | no | Alert instruction. | | `sent` | timestamp | no | Provider sent time. | | `effective` | timestamp | no | Effective time. | | `onset` | timestamp | no | Alert period start. | | `ends` | timestamp | no | Alert period end. | | `expires` | timestamp | no | Provider expiration metadata; not necessarily the alert period end. | | `areaDescription` | string | no | Affected area description. | | `senderName` | string | no | Provider sender name. | | `references` | array | no | Related alerts. | `references[]` entries contain optional `id`, `identifier`, `sender`, and `sent` fields. ## `weather.outlook.v2` Payload type: `WeatherOutlookRun`. The current producer is the SPC convective outlook normalizer. It emits Day 1-3 convective outlook snapshots for categorical, tornado, hail, and wind products that apply to the configured forecast point. Raw SPC bundles remain complete; canonical outlook payloads are filtered to local polygons. All timestamps are UTC. | Field | Type | Required | Notes | |---|---|:---:|---| | `locationId` | string | no | Operator-configured location identifier. | | `locationName` | string | no | Operator-configured location label. | | `latitude` | number | no | Configured point latitude in decimal degrees. | | `longitude` | number | no | Configured point longitude in decimal degrees. | | `asOf` | timestamp | yes | Snapshot time. For SPC, this is the latest outlook issue time when available. | | `issuedAt` | timestamp | no | Latest issue time across outlook features when any feature exists. | | `outlooks` | array | yes | Ordered outlook polygons containing the configured point. | | `discussions` | array | yes | Run-level day discussions for retained outlook days. | `outlooks[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `id` | string | yes | Deterministic weatherfeeder outlook identifier. | | `provider` | string | yes | Current value is `spc`. | | `product` | string | yes | Current value is `convective`. | | `day` | integer | yes | SPC outlook day, currently `1`, `2`, or `3`. | | `outlookType` | string | yes | `categorical`, `tornado`, `hail`, or `wind`. | | `label` | string | yes | SPC outlook label such as `SLGT` or `15`. | | `labelText` | string | no | Human label text from SPC, when present. | | `severityRank` | integer | no | SPC `DN` value, when present. | | `validFrom` | timestamp | yes | Valid period start. | | `validTo` | timestamp | yes | Valid period end. | | `issuedAt` | timestamp | yes | Feature issue time. | | `expiresAt` | timestamp | yes | Expiration time; currently equal to `validTo`. | | `forecaster` | string | no | SPC forecaster text, when present. | | `sourceUrl` | string | no | GeoJSON product URL for this outlook feature. | | `imageUrl` | string | no | Reserved for provider image URLs; currently empty. | | `containsLocation` | boolean | yes | Always `true` for emitted v2 outlooks. | | `geometry` | object | yes | Compact GeoJSON `Polygon` or `MultiPolygon` geometry. | `geometry` preserves the SPC feature geometry as compact GeoJSON using `[longitude, latitude]` coordinate order. `containsLocation` is computed with that geometry and the configured source `latitude`/`longitude`; boundary points count as contained. Polygons that do not contain the configured point are not included in canonical v2 payloads. `discussions[]` entries: | Field | Type | Required | Notes | |---|---|:---:|---| | `day` | integer | yes | SPC outlook day, currently `1`, `2`, or `3`. | | `headline` | string | no | Matching Day 1-3 print-page product title. | | `summary` | string | no | Text from the print-page `...SUMMARY...` section. | | `discussion` | string | no | Cleaned full print-page product text. | | `updatedAt` | timestamp | no | Print-page update time, when present. | When no SPC polygons apply locally, the run is still emitted with `outlooks: []` and `discussions: []`. Discussions are included only for days represented by at least one retained outlook, and multiple retained outlook types for the same day share one discussion entry. ### SPC Outlook Supersession Consumers should prefer latest-run semantics for current conditions: read the latest `WeatherOutlookRun` for the configured location and use its `outlooks` and `discussions` arrays together. Historical SQL consumers that collapse older rows should identify superseded outlooks by `provider`, `product`, `outlookType`, `validFrom`, and `validTo`, then keep rows with the greatest `issuedAt`. `day` and `label` are not identity fields. When multiple retained polygons share that latest `issuedAt`, preserve the full group. ## Legacy `weather.outlook.v1` `weather.outlook.v1` is a historical canonical schema retained as a standards constant for older data and consumers. Current SPC normalization emits `weather.outlook.v2`. The v1 payload used `WeatherOutlookRun` and placed `headline`, `summary`, and `discussion` on each `outlooks[]` polygon. It also represented the pre-v2 SPC canonical behavior, where national polygons were preserved in canonical output. ## Compact Example ```json { "id": "NWSObservationKSTL:2026-06-10T12:00:00Z", "kind": "observation", "source": "NWSObservationKSTL", "emitted_at": "2026-06-10T12:00:05Z", "effective_at": "2026-06-10T12:00:00Z", "schema": "weather.observation.v1", "payload": { "stationId": "KSTL", "timestamp": "2026-06-10T12:00:00Z", "conditionCode": 0, "temperatureC": 22.5 } } ```