Electricity grids already run on models. Every transmission operator forecasts load hours and days ahead, every utility with wind and solar forecasts their output, a state estimator reconstructs voltages and flows from noisy telemetry several times a minute, and a growing fleet of batteries, heat pumps and electric vehicle chargers is dispatched by software through aggregators. Machine learning has moved into each of these: gradient boosted and neural forecasters, learned anomaly detectors, reinforcement learning for battery bidding, and now language-model copilots that summarise alarms and draft switching orders.

That makes the security question specific. An attacker who wants to hurt a grid, or profit from one, does not need to break into a breaker controller if they can quietly bend the numbers that humans and automation trust. This article walks through the four places where AI changes the grid's attack surface: forecasts, state estimation, distributed energy dispatch and control-room assistants, with working controls and a mapping onto the NERC CIP standards for North American operators.

Where AI sits in a grid operator

Start with a map. The diagram below separates three zones: external feeds that the operator does not control, the analytics layer where forecasts and the state estimator run, and the operational side where decisions turn into market bids and control actions. The dashed box is the electronic security perimeter. The important design choice is that nothing learned writes directly to the operational side: forecasts and estimates inform decisions, and any automated action passes through a dispatch envelope that a model cannot widen.

Where AI touches a grid operator, and where each trust boundary sitsExternal feedsweather, prices, DER telemetryField measurementsSCADA RTUs, PMUs, metersForecast modelsload, wind, solarState estimator+ bad-data detectionOperator decisionunit commitment, dispatchMarket bidsday-ahead, reservesControl actionsAGC, DER setpointsintegrity gateauth + plausibilityenvelopeOT side: electronic security perimeter (CIP-005), monitored east-west (CIP-015)Dispatch envelope servicelimits, ramp caps, two-person ruleINSM sensorseast-west traffic baselinesLLM copilot (IT side)read-only replicas, no writesproposal only
Forecasts and state estimation sit between untrusted inputs and consequential outputs. Gates on the left keep inputs honest; the envelope on the right bounds what any automated decision can do.

Three kinds of harm follow from that map. Economic harm comes from a biased forecast that makes the operator over-commit generation or buy reserves at the wrong price; an attacker holding a position in the market can profit from it. Reliability harm comes from a wrong picture of the grid, where the operator or automation acts on flows that do not exist. Physical harm comes from coordinated commands to many small devices at once, which individually look harmless and together move hundreds of megawatts. Each needs a different control, so it helps to be explicit about which one a given model can cause.

Forecast integrity

A load or renewable forecaster is a function of weather forecasts, calendar features, recent telemetry and sometimes price signals. Most of those inputs arrive from outside the operator, over APIs and file drops, and the model has no way to tell a corrupted temperature field from a real cold snap. Poisoning can happen at inference time, by tampering with the live feed, or at training time, by seeding the history the model is retrained on so it learns a bias that fires under specific conditions.

The defence is to treat a forecast as a claim that must agree with independent evidence before it drives a decision. Run a cheap, structurally different baseline next to the production model: a similar-day or persistence forecast that uses no external feed at all. Cross-check each external input against a second source where one exists, such as two weather providers or the operator's own weather stations. When the production forecast and the baseline disagree by more than their historical error band, the system should not silently pick one; it should flag the hour, show both to the operator and fall back to the conservative choice.

from dataclasses import dataclass

@dataclass
class ForecastCheck:
    hour: str
    model_mw: float
    baseline_mw: float          # similar-day forecast, no external feeds
    band_mw: float              # p99 historical |model - baseline| for this hour type
    temp_primary_c: float
    temp_secondary_c: float     # second provider or own weather stations

def judge(fc: ForecastCheck, prefer_high: bool, temp_tol_c: float = 3.0) -> dict:
    reasons = []
    if abs(fc.temp_primary_c - fc.temp_secondary_c) > temp_tol_c:
        reasons.append("weather feeds disagree")
    if abs(fc.model_mw - fc.baseline_mw) > fc.band_mw:
        reasons.append("model outside baseline band")
    if not reasons:
        return {"hour": fc.hour, "use": fc.model_mw, "status": "ok"}
    # Conservative fallback. Load: plan high. Wind/solar output: plan low.
    pick = max if prefer_high else min
    return {"hour": fc.hour, "use": pick(fc.model_mw, fc.baseline_mw),
            "status": "review", "reasons": reasons}

For training-time poisoning, keep retraining data content-addressed and versioned, retrain from raw telemetry rather than from previously cleaned exports that someone could have edited, and evaluate every candidate model on a frozen backtest that includes known extreme days. A model that suddenly does better on ordinary days and worse on the frozen extremes deserves suspicion before promotion.

State estimation and false data injection

The state estimator is the grid's sense of sight. It takes hundreds or thousands of measurements z, relates them to the unknown state x (bus voltage magnitudes and angles) through a measurement model H, and solves a weighted least squares problem. Classical bad-data detection then checks the residual, the gap between what was measured and what the estimated state predicts. A broken meter or a crude tampering attempt produces a large residual and gets flagged.

The uncomfortable result, published by Liu, Ning and Reiter in 2009, is that an attacker who knows H can add an attack vector of the form a = Hc to the measurements. The estimator then shifts its answer by exactly c and the residual does not change at all, so the test passes while the operator sees a false state. The linearised example below demonstrates it in a few lines.

Why a residual test alone misses a coordinated false data injectionMeasurements zz = Hx + eWLS estimatex_hat from zResidual test||z - H x_hat|| vs thresholdRandom corruptionresidual jumps: caughtStealthy attack a = Hcresidual unchanged: missedDefencesprotected basis, PMU, temporalclose the gap
Residual-based detection catches inconsistent corruption, but an attack constructed in the column space of H is invisible to it by construction.
import numpy as np
rng = np.random.default_rng(0)

H = rng.normal(size=(12, 4))            # 12 measurements, 4 state variables (DC model)
x_true = rng.normal(size=4)
z = H @ x_true + rng.normal(scale=0.01, size=12)

def estimate(z):
    x_hat, *_ = np.linalg.lstsq(H, z, rcond=None)
    return x_hat, np.linalg.norm(z - H @ x_hat)

_, r_clean = estimate(z)
_, r_noise = estimate(z + np.eye(12)[3] * 0.5)      # one crude tampered meter
c = np.array([0.0, 0.3, 0.0, 0.0])                  # attacker's chosen state shift
x_bad, r_stealth = estimate(z + H @ c)

print(f"clean {r_clean:.4f}  crude {r_noise:.4f}  stealth {r_stealth:.4f}")
print("state shift seen by operator:", np.round(x_bad - x_true, 3))

Run it and the crude attack lifts the residual by more than an order of magnitude, while the stealthy one leaves it equal to the clean case and moves the second state variable by about 0.3. Learned anomaly detectors trained on residual statistics inherit the same blind spot, so the defences have to change what the attacker can reach rather than what the detector looks at. Authenticate and protect a basis set of measurements, enough independent ones that no consistent attack is possible without compromising a protected one; phasor measurement units with authenticated streams are the usual choice. Add temporal checks, because a real state evolves smoothly under physics and load patterns while an injected one often steps. Keep H itself confidential, since the full model of the network is what makes the stealthy attack constructible.

Distributed energy: agents that move megawatts

The newest surface is distributed energy. A virtual power plant aggregates thousands of batteries, inverters, thermostats and chargers and offers their combined flexibility to the grid. Software, increasingly learned policies, decides when each device charges or discharges. A single device is small; the fleet is not. Ten thousand home batteries at 5 kW each are 50 MW, and if they all switch in the same second the local distribution network sees a step it was never planned for.

Put a dispatch envelope service between any optimiser, learned or not, and the device API. The optimiser proposes; the envelope enforces per-device limits, fleet-wide ramp caps, per-feeder caps from the distribution operator and a staggered schedule. Envelope limits live in configuration that requires two approvers to change, and the service is the only holder of the device-command credential.

def enforce(proposals, cfg, last_fleet_mw):
    # proposals: list of (device_id, feeder, mw) with +discharge / -charge
    out, feeder_tot, fleet = [], {}, 0.0
    for dev, feeder, mw in proposals:
        mw = max(-cfg.dev_max_mw, min(cfg.dev_max_mw, mw))           # per-device clamp
        if abs(feeder_tot.get(feeder, 0.0) + mw) > cfg.feeder_cap_mw[feeder]:
            continue                                                  # drop, never widen
        feeder_tot[feeder] = feeder_tot.get(feeder, 0.0) + mw
        fleet += mw
        out.append((dev, mw))
    step = fleet - last_fleet_mw
    if abs(step) > cfg.fleet_ramp_mw_per_min:
        target = last_fleet_mw + cfg.fleet_ramp_mw_per_min * (1 if step > 0 else -1)
        factor = target / fleet if fleet else -1.0
        if not 0.0 <= factor <= 1.0:
            return HOLD_PREVIOUS                                      # scaling may only shrink
        out = [(d, m * factor) for d, m in out]
    return stagger(out, window_s=cfg.stagger_s)                       # spread over time

The ramp scaling shown is deliberately simple; the point is that the cap is applied outside the optimiser and cannot be argued away by it. Log every proposal next to what was actually sent, so a policy that keeps hitting the envelope is visible as a model problem instead of silently being corrected forever.

Control-room copilots

Language-model assistants in the control room are useful for alarm triage, procedure lookup and drafting switching orders. They are also a path for indirect prompt injection: an alarm text, a vendor bulletin or an outage ticket can contain instructions the model will follow. Keep the assistant on the IT side, reading replicated historian data and documents, with no credential that can write to operational systems. Its output is a proposal that an operator executes through the normal tools. The design is covered in detail in the water utility article, whose bounded-assistant pattern transfers directly, and the review step should follow the guidance in human-in-the-loop for high-risk actions.

Mapping to NERC CIP

For North American bulk electric system entities, the controls above map onto existing NERC CIP requirements. Treat the mapping as an engineering starting point for a conversation with your compliance team, not as a compliance determination.

StandardWhat it coversHow AI systems fit
CIP-002Categorising BES cyber systemsDecide whether a forecaster or estimator host is in scope; a model that feeds automatic control usually is
CIP-005Electronic security perimeters and remote accessCopilots and training pipelines stay outside; data crosses one way via replicas
CIP-007Ports, patching, malicious code, loggingML runtimes and their Python dependencies are software to patch and log
CIP-010Configuration change managementTreat a new model version or threshold as a baseline change with testing evidence
CIP-011Protecting BES cyber system informationNetwork models (H) and topology sent to a cloud model are sensitive information
CIP-013Supply chain risk managementModel vendors, weather providers and aggregator platforms belong in the plan
CIP-015Internal network security monitoringBaseline east-west traffic, including model-serving hosts inside the perimeter

CIP-015-1 is the newest entry. FERC approved it in Order No. 907 in June 2025 with an effective date of 2 September 2025; control centres have 36 months to comply and other applicable systems 60. A model server inside the perimeter that suddenly talks to a new host is exactly the east-west anomaly that monitoring is meant to surface.

Worked example: a poisoned wind forecast

Consider a regional operator on a windy autumn evening. At 16:00 the wind forecast for 19:00 shows 2,400 MW, the similar-day baseline shows 1,500 MW, and the historical p99 gap for that hour type is 600 MW. The forecast check fires. Looking at inputs, the primary weather provider's hub-height wind speed for two large farms is 4 m/s higher than both the secondary provider and the farms' own met masts.

Without the gate, the operator would have planned for 900 MW of wind that would not arrive and covered the shortfall in real time at scarcity prices, or worse, run short on reserves. With it, the hour is planned on the baseline, the feed is quarantined and the provider is contacted. Whether the cause was tampering or a provider bug, the response is identical, and the forecast pipeline records which input caused the deviation so the retraining set excludes it.

Failure modes

  • Gates tuned so tight they fire daily. Operators learn to click through. Tune bands on a year of history and report the alert rate weekly.
  • A baseline that shares the attacked input. If both models read the same weather feed, the cross-check proves nothing. Independence is the whole point.
  • Learned detectors trusted against adaptive attackers. They catch faults well and constructed attacks poorly; pair them with authenticated measurements.
  • Envelope limits that drift upward. Every temporary widening for an event becomes permanent. Expire changes automatically.
  • Network models leaking. Topology and impedance data shared with a vendor or pasted into a cloud assistant hand an attacker H.

Trade-offs

Every gate costs some value: a conservative fallback buys reserves you may not need, a ramp cap leaves flexibility unsold, and read-only copilots mean operators still type the commands. Those costs are small and predictable; the failures they prevent are large and rare. The harder call is investment in authenticated measurement, expensive to retrofit but the only robust answer to constructed injection.

What to do next

  1. List every model that influences a market bid or control action and tag which of the three harms it can cause.
  2. Add an independent baseline forecast and a second weather source, and log disagreement rates for a month before enforcing.
  3. Run the residual demo against your own linearised network to show leadership why authenticated measurements matter.
  4. Put a dispatch envelope with fleet and feeder caps in front of every DER optimiser.
  5. Keep LLM assistants on replicas with no write credentials, and red-team them with injected alarm text (see indirect prompt injection).
  6. Version and freeze training data with a backtest of extreme days, following the practices in data poisoning attacks.
  7. Walk the CIP mapping with compliance and add model versions to change management.
Key takeaway: On a grid, the dangerous AI failure is a believable wrong number, not a dramatic intrusion. Check forecasts against independent baselines, close the stealthy-injection gap with authenticated measurements rather than smarter detectors, bound every automated dispatch with an envelope the model cannot change, keep assistants read-only, and record model changes as configuration changes.