Public Alerting & CAP Message Pipelines

An evacuation order is issued for a 4-square-kilometre polygon on the north edge of a fire. The Common Alerting Protocol (CAP) message carries the polygon exactly. The Wireless Emergency Alert derived from it reaches every handset attached to a cell sector that touches the polygon, which includes a sector whose tower serves a town 11 kilometres away. Two thousand people evacuate who were never in danger, and the roads they use are the ones the evacuation needed.

Problem Framing

Public alerting is the one workflow on this site whose output is read by the public rather than by responders, and that changes the failure modes completely. A defect in the common operating picture is seen by trained people who can question it; a defect in an alert is acted on immediately by tens of thousands of people who cannot. Over-alerting has a measurable cost in road capacity and in future compliance, and under-alerting has an obvious one.

The specific difficulty is that the alert area a responder authors is not the area that gets delivered. Every channel reshapes it, in a direction the author cannot see from the authoring tool.

One incident polygon becoming three differently-shaped alert areas A single evacuation polygon is delivered through three channels, each of which reshapes it. The Common Alerting Protocol message itself carries the polygon verbatim, up to a practical vertex limit imposed by message size. A Wireless Emergency Alert is delivered by cell broadcast, so the area becomes the union of the cell sectors that intersect the polygon, which extends past it wherever a sector reaches beyond the boundary. An Emergency Alert System broadcast is delivered by geographic code, so the area becomes whole counties or FIPS subdivisions containing any part of the polygon. The three areas are nested and progressively coarser, and a resident's experience depends entirely on which channel reached them — which is why the same alert must state its intended area, not only its delivered one. one polygon, three delivery geometries county boundary — EAS cell sectors — WEA the polygon — CAP CAP polygon · as authored verbatim, subject to a vertex budget WEA · union of intersecting cell sectors reaches past the boundary wherever a sector does EAS · whole geographic codes any county touching the polygon, entire A resident's experience depends on which channel reached them. So the message must record the intended area, not only the delivered one.

The three areas are nested and progressively coarser, and each is correct for its channel. What goes wrong is treating the authored polygon as the delivered area — sizing a polygon tightly to avoid over-alerting, and then discovering that the cell-sector union it produced was four times larger than intended.

Prerequisites

  • A CAP 1.2 authoring library and the official XSD, plus the specific profile of whichever alerting authority you submit through. CAP is a standard with per-authority profiles, and a message can be schema-valid and profile-invalid.
  • An authoritative incident polygon in EPSG:4326 with axis order settled per the coordinate reference system standard — an inverted alert polygon is a category of error with an unusually large blast radius.
  • The geographic code set your EAS path uses and, where available, the cell-sector geometry the WEA path will map onto, so the delivered areas can be estimated before release rather than observed afterwards.
  • A named human approver. Nothing in this topic removes that requirement, and the design below is built around making their decision easy rather than replacing it.

The Fields That Carry Meaning

The CAP fields that decide what a device does with a message Five Common Alerting Protocol fields carry the operational meaning. The identifier, sender and sent timestamp together form the message's unique key, and a re-sent alert reusing an identifier is a different message to some consumers and the same one to others. Message type distinguishes an alert from an update, a cancel or an error, and it is what lets a device replace rather than accumulate. References names the prior identifiers an update supersedes, and omitting it turns an update into a second independent alert. Urgency, severity and certainty drive whether a handset presents the message immediately or silently. Effective, onset and expires bound the message's life, and an absent expires leaves an alert live indefinitely on devices that never receive the cancel. five fields carry the operational meaning — the rest is presentation identifier · sender · sent the unique key — reusing an identifier is a new message to some consumers and the same one to others msgType Alert · Update · Cancel · Error — what lets a device replace a message rather than accumulate one references the prior identifiers this supersedes — omit it and an Update becomes a second independent alert urgency · severity · certainty whether a handset presents immediately or silently — not free-text priority effective · onset · expires an absent expires leaves the alert live forever on any device that never receives the cancel

Most CAP elements are presentation. Five carry operational behaviour, and two of those are the ones most often got wrong.

references is the field that makes an update an update. A CAP message with msgType of Update but no references is, to most consumers, simply a second alert — so a device that received both now shows two evacuation orders with different boundaries and no indication which supersedes the other. That is worse than not updating.

expires is the field that bounds the damage from everything else. Devices lose messages; a cancel that never arrives leaves the alert live forever unless it expires on its own. Setting a conservative expiry and re-issuing while the hazard persists is strictly safer than a long expiry and a cancel you are trusting to arrive.

Validating Before Release

Where a CAP message is validated, and what each gate catches Four validation stages before a Common Alerting Protocol message is released. Schema validation against the CAP 1.2 XSD catches structural errors and missing mandatory elements, and costs milliseconds. Profile validation against the receiving authority's own profile catches values that are legal in CAP and rejected by that gateway, such as an unsupported event code. Geometry validation checks the polygon is closed, wound correctly, within the sender's jurisdiction and inside the vertex budget the channel imposes. Operational validation is the only human step: it confirms the message says what the incident commander intended, in language a resident will act on. Each stage catches what the one before it cannot, and only the last one can catch a technically perfect alert that tells people to do the wrong thing. four gates — and only the last catches a perfect message that says the wrong thing 1 · schema — CAP 1.2 XSD structure and mandatory elements · milliseconds · fully automatable 2 · profile — the receiving authority's own values legal in CAP and rejected by that gateway — an unsupported event code, a disallowed category 3 · geometry closed · wound correctly · inside the sender's jurisdiction · within the channel's vertex budget 4 · operational — a human, always does it say what the incident commander meant, in words a resident will act on? Nothing automates this.

Each gate catches what the one before it cannot, and the ordering matters because the cheap ones eliminate most failures before a human is asked to look. Schema validation is milliseconds and catches structural defects. Profile validation catches the values that are legal in CAP and rejected by a particular gateway — this is where an event code that works in one state’s system fails in a neighbour’s.

Geometry validation is where this site’s usual concerns land: a closed ring, correct winding, inside the sender’s jurisdiction, and within the vertex budget the channel imposes. A polygon exceeding the budget is not rejected by most gateways; it is simplified, and the simplification is not yours.

The fourth gate is human and cannot be automated away. A message can pass all three technical gates and tell people to shelter in place when the incident commander ordered an evacuation.

Step-by-Step Implementation

python
from __future__ import annotations

import logging
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

from lxml import etree
from shapely.geometry import Polygon
from shapely.geometry.base import BaseGeometry

logger = logging.getLogger("incidentgis.cap")

CAP_NS = "urn:oasis:names:tc:emergency:cap:1.2"
# Practical ceiling before gateways simplify the polygon themselves.
MAX_ALERT_VERTICES = 100


@dataclass(frozen=True)
class AlertIntent:
    """What the incident commander decided, separate from how it is delivered."""
    event: str
    headline: str
    instruction: str
    urgency: str        # Immediate | Expected | Future | Past | Unknown
    severity: str       # Extreme | Severe | Moderate | Minor | Unknown
    certainty: str      # Observed | Likely | Possible | Unlikely | Unknown
    approver: str
    supersedes: tuple[str, ...] = ()


def _cap_polygon(geom: BaseGeometry) -> str:
    """CAP polygons are lat,lon pairs, space separated, first point repeated.

    Note the coordinate order: CAP is latitude first, the opposite of GeoJSON.
    Emitting lon,lat here produces a syntactically valid alert for somewhere
    else entirely, which is the highest-consequence axis-order bug on the site.
    """
    if not isinstance(geom, Polygon):
        raise ValueError("alert areas must be a single closed polygon")
    ring = list(geom.exterior.coords)
    if len(ring) > MAX_ALERT_VERTICES:
        raise ValueError(
            f"{len(ring)} vertices exceeds the {MAX_ALERT_VERTICES} budget; "
            "simplify deliberately rather than letting the gateway do it"
        )
    return " ".join(f"{lat:.6f},{lon:.6f}" for lon, lat in ring)


def build_alert(
    intent: AlertIntent,
    area: BaseGeometry,
    *,
    sender: str,
    expires_in: timedelta = timedelta(hours=2),
    identifier: str | None = None,
) -> bytes:
    """Build a CAP 1.2 alert from an incident polygon and a stated intent."""
    if intent.supersedes and not identifier:
        identifier = str(uuid.uuid4())
    now = datetime.now(timezone.utc)
    msg_type = "Update" if intent.supersedes else "Alert"

    root = etree.Element("alert", nsmap={None: CAP_NS})
    etree.SubElement(root, "identifier").text = identifier or str(uuid.uuid4())
    etree.SubElement(root, "sender").text = sender
    etree.SubElement(root, "sent").text = now.isoformat()
    etree.SubElement(root, "status").text = "Actual"
    etree.SubElement(root, "msgType").text = msg_type
    etree.SubElement(root, "scope").text = "Public"

    if intent.supersedes:
        # Without references an Update is just a second alert, and a device
        # that received both shows two orders with no way to rank them.
        etree.SubElement(root, "references").text = " ".join(intent.supersedes)

    info = etree.SubElement(root, "info")
    etree.SubElement(info, "category").text = "Safety"
    etree.SubElement(info, "event").text = intent.event
    etree.SubElement(info, "urgency").text = intent.urgency
    etree.SubElement(info, "severity").text = intent.severity
    etree.SubElement(info, "certainty").text = intent.certainty
    # A conservative expiry that is re-issued beats a long one plus a cancel
    # you are trusting to arrive on every device.
    etree.SubElement(info, "expires").text = (now + expires_in).isoformat()
    etree.SubElement(info, "headline").text = intent.headline
    etree.SubElement(info, "instruction").text = intent.instruction

    area_el = etree.SubElement(info, "area")
    etree.SubElement(area_el, "areaDesc").text = intent.headline
    etree.SubElement(area_el, "polygon").text = _cap_polygon(area)

    payload = etree.tostring(root, xml_declaration=True, encoding="UTF-8")
    logger.info("cap_alert_built", extra={
        "msg_type": msg_type, "approver": intent.approver,
        "vertices": len(list(area.exterior.coords)),
        "supersedes": intent.supersedes,
    })
    return payload

Configuration Reference

Parameter Env var Default Notes
Sender identifier CAP_SENDER unset The registered sender ID; a message with the wrong one is rejected at the gateway.
Default expiry CAP_EXPIRES_MINUTES 120 Short and re-issued beats long and cancelled.
Vertex budget CAP_MAX_VERTICES 100 Above this the gateway simplifies for you, with a shape you did not choose.
Simplification tolerance CAP_SIMPLIFY_M 50 Applied deliberately when over budget, in a projected CRS.
Overshoot warning ratio CAP_OVERSHOOT_WARN 1.5 Warn the approver when the estimated delivered area exceeds the authored one by this factor.
Profile CAP_PROFILE unset The receiving authority’s profile; schema-valid is not the same as accepted.
Approver required CAP_REQUIRE_APPROVER true There is no supported value of false.

Verification and Smoke Test

Validate against the XSD, then against the profile, then assert the polygon round-trips:

bash
xmllint --noout --schema CAP-v1.2.xsd alert.xml && echo "schema ok"
python -m incidentgis.cap.profile_check --profile state-ipaws alert.xml
python -m incidentgis.cap.geometry_check --jurisdiction county-bernalillo alert.xml

The geometry check must confirm the emitted polygon parses back to the polygon that was authored — the latitude-first ordering makes a silent transposition the single highest-consequence defect this pipeline can ship.

Integration With Adjacent Workflows

The alert polygon comes from the reconciled perimeter that the conflict resolver produced, so an alert inherits every property of that reconciliation including its audit trail. Evacuation instructions should be consistent with the routes the evacuation routing layer is publishing, since an alert telling people to leave via a road the router has closed is worse than no alert.

Troubleshooting

Symptom: the gateway rejects a message that validates against the XSD. It is failing the authority’s profile. Compare the event code and category against that authority’s list, not against CAP’s.

Symptom: an update appears as a second alert on handsets. references is missing or names an identifier that was never sent. It must list the exact prior identifiers, space separated.

Symptom: the delivered area is far larger than the polygon. That is cell-sector or geographic-code delivery working normally. Estimate it before release and show the approver the ratio.

Symptom: the alert polygon appears in the wrong hemisphere. CAP is latitude-first and GeoJSON is longitude-first. Assert a round-trip parse, not a visual check.

Symptom: an old alert is still showing on some devices days later. expires was long or absent and the cancel did not reach them. Shorten the expiry and re-issue while the hazard persists.

Frequently Asked Questions

Why is the delivered alert area larger than the polygon we drew? Because each channel reshapes it. The CAP message itself carries the polygon verbatim, but a Wireless Emergency Alert is delivered by cell broadcast, so the effective area becomes the union of every cell sector intersecting the polygon and extends wherever a sector reaches beyond the boundary. An Emergency Alert System broadcast is delivered by geographic code, so it covers entire counties or subdivisions containing any part of the polygon. The three areas are nested and progressively coarser, and sizing a polygon tightly to limit over-alerting does not limit what the coarser channels deliver — which is why the estimated delivered area should be shown to the approver before release.

What makes a CAP update behave as an update rather than a new alert? The references element. A message with msgType set to Update but no references is, to most consumers, simply a second independent alert, so a device that received both displays two evacuation orders with different boundaries and no way to tell which supersedes the other — worse than not having updated at all. References must list the exact prior identifiers, space separated, and those identifiers must be ones that were actually sent. The related discipline is expires: devices lose messages, so a cancel that never arrives leaves an alert live indefinitely, and a short expiry that is re-issued while the hazard persists is safer than a long one plus a cancel you are trusting to arrive.

What is the highest-consequence technical defect in a CAP pipeline? Transposed coordinates. CAP polygons are written latitude first, which is the opposite of GeoJSON’s longitude-first ordering, so a pipeline that emits its internal representation directly produces a syntactically valid alert describing somewhere else entirely. It passes schema validation, passes profile validation, and is delivered. The defence is to parse the emitted polygon back and assert it equals the authored geometry, rather than checking it visually in an authoring tool that may use the same wrong convention on the way back in.

Up: Incident Mapping & Multi-Agency Sync Workflows

Continue inside this section

Other guides in Incident Mapping & Multi-Agency Sync Workflows: Production Architecture for Python Emergency Response