# US Interconnection Queue Tracker - 7 ISO Queues & Deltas API (`malonestar/interconnection-queue-tracker`) Actor

Normalize US ISO/RTO generator interconnection queues (SPP, MISO, NYISO, CAISO, PJM, ERCOT, ISO-NE) into one schema and track new, withdrawn, status-change and COD-slip deltas. For renewables developers, land agents, and energy consultants. Keyless.

- **URL**: https://apify.com/malonestar/interconnection-queue-tracker.md
- **Developed by:** [Kyle Maloney](https://apify.com/malonestar) (community)
- **Categories:** Lead generation, Developer tools, Agents
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.40 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## US Interconnection Queue Tracker & Delta Monitor

Pulls the generator interconnection queues that US grid operators (ISOs/RTOs) publish, normalizes every one of them into a single schema, and can emit a delta feed of what changed since your last run: new requests, withdrawals, status changes, and COD slips (commercial-operation dates pushed later). Built for renewables and storage developers, land scouts, and grid analysts who otherwise have to hand-scrape a different portal per region.

### How it works

Each ISO ships its queue in a different format and column vocabulary. This actor fetches the raw file, page, or API per ISO, maps it through a per-ISO adapter into one flat record shape, and either returns that snapshot or diffs it against a prior one. No logins and no API keys — every source below is a public file, page, or endpoint.

| ISO / RTO | Source | Format | Notes |
|---|---|---|---|
| SPP (Southwest Power Pool) | `opsportal.spp.org` GenerateActiveCSV | CSV | Active queue only |
| MISO | `misoenergy.org/api/giqueue/getprojects` | JSON | ~3.8k projects |
| NYISO | `NYISO-Interconnection-Queue.xlsx` | xlsx | All three active tabs merged: "Interconnection Queue", " Cluster Projects" (the post-2024 cluster queue) and "Load Projects" (load interconnections, the only source in the federation that populates `interconnection_type: "load"`). ~264 rows. |
| CAISO | `publicqueuereport.xlsx` | xlsx | "Grid GenerationQueue" sheet |
| PJM | `services.pjm.com/PJMPlanningApi/api/Queue/ExportToXls` | xlsx | Full queue history since 1997, ~9.3k rows. Keyless — uses the same public subscription key PJM's own public interconnection-queues page calls client-side, no PJM account needed. If PJM rotates that key the actor warns and skips PJM for that run rather than failing. |
| ERCOT | Monthly GIS Report, resolved from the public ERCOT MIS report list (`ercot.com/misapp/GetReports.do`) | xlsx | Merges the "Large Gen" and "Small Gen" tabs, ~1.85k rows. The download URL changes every month (new file, new report ID) so it's re-resolved on every run. Texas-only grid — `state` is hardcoded `TX`. Cancelled/inactive projects are published in separate ERCOT report tabs and are not merged in this version. |
| ISO-NE | `irtt.iso-ne.com/reports/external` public queue page | HTML table | Server-rendered, no login. The page 302-redirects once and requires forwarding its `AspxAutoDetectCookieSupport` cookie back on the follow-up request — the actor handles that automatically. ~1.75k rows. |

Leave `isos` empty to pull all 7 live sources in one run. Adding a new ISO is one config block (`{ iso, format, url, map }`) in `source.js`.

#### Field coverage varies by ISO

Not every ISO publishes every field:

- `project_name` is null for SPP (its active-queue CSV doesn't name projects).
- `queue_date` (date the request was filed) is null for ERCOT's Large Gen tab — ERCOT doesn't publish that column there; for Small Gen, `queue_date` is a proxy (`Model Ready Date`, the closest milestone ERCOT publishes).
- `poi` for PJM is the interconnecting Transmission Owner (e.g. "PSEG"), not a specific substation — PJM's public export has no substation/POI column.
- `withdrawn_date` is only populated where the source carries a real withdrawal date/status: SPP, PJM, ISO-NE. NYISO, CAISO, MISO, and ERCOT don't expose one (ERCOT publishes cancellations in a separate report not merged here).
- `study_phase` doubles as a catch-all "current milestone/cluster" field and its meaning differs by ISO (SPP/PJM: cluster or project type; ERCOT Large Gen: folded into `status` instead since ERCOT has no separate phase column; ERCOT Small Gen: constant `"Small Generator"`; ISO-NE: the ISO's internal cluster code).

Dates are normalized to `YYYY-MM-DD`; month-only values snap to the first of the month.

### Delta mode (legacy manual)

In `delta` mode the actor compares the current snapshot to a prior one YOU supply and emits only change rows. Supply the prior records directly as `priorItems`, or set `priorKvKey` to a key in the actor's key-value store — it diffs against the snapshot stored there and writes the fresh snapshot back, so a daily schedule automatically emits only what moved. A COD slip is flagged when a project's requested commercial-operation date moved later than the prior run; a withdrawal is flagged from a withdrawn date, a terminate/cancel status, or a record dropping out of the queue.

### Delta & monitoring mode (automatic — no input wiring needed)

Every `snapshot`-mode run (the default) now also tracks itself automatically. The actor keeps its own snapshot in the key-value store under the key `QUEUE_SNAPSHOT` — you don't supply anything. Each run:

- Tags every queue record with `is_new_since_last_run` (not present in the last saved snapshot), `status_changed`, and `previous_status` (the status recorded last time, when it changed).
- Emits a synthetic `row_type: "withdrawn"` row for any project that was in the last snapshot but is missing from this run's pull — but only for ISOs that were fetched successfully this run. If an ISO's source errors out, its prior projects are never falsely marked withdrawn; the actor just has no fresh data for them yet.
- Emits one `row_type: "iso_summary"` roll-up row per ISO: `total_projects`, `status_counts` (by coarse bucket: active / withdrawn / completed / suspended / unknown), `total_capacity_mw`, `new_count`, `withdrawn_count`, `status_changed_count`.

Set `deltaOnly: true` to get a lean monitoring feed: unchanged rows are suppressed and the run emits only the new/status-changed rows, the withdrawn rows, and the iso\_summary roll-ups — ideal for a scheduled daily or weekly monitor where you only care what moved. Leave it `false` (default) to keep getting the full queue every run, with these fields as bonus context.

The very first run ever (no snapshot exists yet) writes a baseline silently: every row's `is_new_since_last_run` stays `false` on that run (otherwise the entire queue would look "new"). From the second run onward, deltas are real. Row identity is `iso + queue_id`, or (for the rare row with no native queue ID) `iso` + a hash of project name + capacity — stable across runs as long as the project's name/capacity don't change.

This monitor engine is completely independent of the legacy `delta` mode above — existing `priorItems`/`priorKvKey` workflows are untouched.

### Example input — snapshot, multiple ISOs

```json
{ "isos": ["PJM", "ERCOT", "ISO-NE"], "mode": "snapshot", "maxResults": 5000 }
```

### Example input — delta (scheduled monitoring)

```json
{ "isos": ["SPP"], "mode": "delta", "priorKvKey": "spp_last_snapshot" }
```

### Example input — automatic monitor mode (deltaOnly)

```json
{ "isos": ["SPP", "MISO", "PJM"], "mode": "snapshot", "deltaOnly": true }
```

No `priorKvKey` needed — the actor tracks its own snapshot under the key-value-store key `QUEUE_SNAPSHOT`. Run this on a daily or weekly Apify Schedule to get only new projects, status changes, and withdrawals since the last run.

### Output fields

Every declared field, in full. All fields are nullable — `null` means **not published / not checked**, never "checked and zero".

#### Per-project fields (`row_type: "queue_record"`)

| Field | What it is |
|---|---|
| `iso` | Grid operator the record came from: SPP, MISO, NYISO, CAISO, PJM, ERCOT, ISO-NE |
| `queue_id` | ISO-assigned interconnection queue / project number |
| `project_name` | Project name where the ISO publishes one |
| `interconnection_type` | Closed vocabulary: `generation`, `storage`, `load`, `transmission` |
| `request_category` | Closed vocabulary: `generator_interconnection`, `transmission_service`, `upgrade`, `load`, `affected_system` |
| `fuel_type` | Fuel / technology as published (codes decoded per ISO) |
| `capacity_mw` | Requested / net capacity in MW |
| `nameplate_capacity_mw` | Second published capacity figure (nameplate, winter net, MW in service) — compare with `capacity_mw` for seasonal or ERIS/NRIS derating |
| `storage_capability_mwh` | Energy storage capability in MWh (NYISO only) |
| `county` | County or nearest town of the point of interconnection |
| `state` | State of the point of interconnection |
| `zone` | Load / planning zone or study group |
| `poi` | Substation, line, or station where the project connects |
| `transmission_owner` | Interconnecting transmission owner or utility |
| `developer` | Developer / interconnection customer (NYISO and ISO-NE only) |
| `service_type` | Interconnection service requested, in the ISO's own vocabulary (ER/NR, ERIS/NRIS, Capacity or Energy) |
| `status` | Current queue status as published by the ISO |
| `study_phase` | Study phase or cluster the request sits in |
| `queue_date` | Date the request entered the queue (`YYYY-MM-DD`) |
| `requested_cod` | Requested commercial-operation / in-service date |
| `withdrawn_date` | Date the request was withdrawn, where the source publishes one |
| `withdrawn_remarks` | Free-text remark (PJM withdrawal remarks, SPP cause of delay, ERCOT comment, MISO post-GIA status, ISO-NE jurisdiction, NYISO load end-use) |
| `source_dataset` | Which feed or workbook tab inside the ISO the row came from |
| `source_url` | The exact endpoint or monthly file the record was pulled from |
| `retrieved_at` | ISO 8601 timestamp of this run |

#### Monitor fields (snapshot mode, on every row)

| Field | What it is |
|---|---|
| `row_type` | `queue_record`, `withdrawn` (synthetic — a project that vanished from the latest pull), or `iso_summary` |
| `is_new_since_last_run` | Not present in the last saved snapshot |
| `status_changed` | Status differs from the last saved snapshot |
| `previous_status` | Status recorded last time, when it changed |
| `previous_requested_cod` | `withdrawn` rows: requested COD recorded before the project disappeared |
| `iso_monitor_baseline` | True on the first run that ever saw this ISO — its records are baselined, not reported as new |

#### Per-source status (on every row)

| Field | What it is |
|---|---|
| `iso_status` | `ok` (fetched in full, passed every live drift check), `truncated`, `not_fetched`, or `unavailable` |
| `isos_requested` | Comma-separated list of every ISO this run asked for |
| `isos_ok` | ISOs that answered in full |
| `isos_unavailable` | ISOs that failed, drifted, were truncated, or were never fetched — `null` when the whole federation answered |
| `run_is_partial` | True when at least one requested ISO did not answer in full |

#### Roll-up fields (`row_type: "iso_summary"`, one per requested ISO)

| Field | What it is |
|---|---|
| `total_projects` | Queue records for this ISO this run — **`null`, not `0`, when the ISO did not answer** |
| `status_counts` | Counts by coarse bucket: active / withdrawn / completed / suspended / unknown |
| `total_capacity_mw` | Sum of `capacity_mw` across this ISO's records |
| `capacity_missing_count` | How many of this ISO's records published no capacity at all and contributed nothing to that sum |
| `new_count`, `withdrawn_count`, `status_changed_count` | Monitor counts for this ISO |
| `iso_row_count`, `iso_fetched_row_count` | Rows emitted, and rows retrieved upstream before any `maxResults` cut |
| `iso_truncated` | The run-wide `maxResults` cap cut this ISO short |
| `iso_error` | Why this ISO did not answer in full |
| `iso_data_as_of` | Publisher-declared as-of date (SPP CSV preamble, ERCOT monthly report posting date) |
| `iso_source_url` | The exact endpoint or monthly file this ISO was read from |
| `withdrawal_inference_eligible` | Whether absence from this pull may be read as a withdrawal for this ISO |

#### Legacy `delta`-mode fields

`change_type` (`new` / `withdrawn` / `status_change` / `cod_slip`), `old_status`, `new_status`, `old_requested_cod`, `new_requested_cod`.

### Reliability: what happens when a source goes down

Seven federated sources means a partial answer is the normal failure mode, not the exception. Three of them (ERCOT's monthly report URL, ISO-NE's HTML table, PJM's public subscription key) are scraped or derived formats where an upstream change returns HTTP 200 and a silently empty parse.

Every ISO is therefore checked **live, on every run**, against a band measured on 2026-08-01: row count inside its envelope, `queue_id` and `status` fill rates above their floors, every `interconnection_type` and `request_category` inside its closed vocabulary, ERCOT rows inside Texas, and — for the two sources that publish one — a declared as-of date no older than its limit. A source that fails any of these is reported `iso_status: "unavailable"`; it is **not** reported as an empty queue. Every measured value is written to the run log.

Consequences, all of them deliberate:

- **A partial run is visible on the row.** `run_is_partial`, `isos_ok` and `isos_unavailable` are stamped on every record, and each ISO that did not answer gets its own `iso_summary` row with `total_projects: null` and the upstream error.
- **Withdrawal inference is disabled for any ISO that did not answer in full.** A project that is missing because its ISO was unreachable is not a withdrawal. This also covers an ISO cut short or skipped by the `maxResults` cap.
- **The snapshot is merged forward, never overwritten.** Prior entries for an ISO that did not answer are carried over, so that ISO does not report its whole queue as brand-new on the following run.
- **The first sighting of an ISO is a baseline.** Adding an ISO to an existing monitor does not fabricate a flood of "new project" events.
- **If every requested ISO fails, the run fails.** Nothing is emitted and nothing is billed.
- **An unrecognised ISO code fails the run** with the list of valid codes, rather than being silently dropped (which used to fall through to pulling all seven).

### Pricing

Pay-per-result: you are charged per dataset record emitted. A full 7-ISO snapshot is roughly 18,200 records; a single ISO ranges from ~270 (CAISO, NYISO) to ~9,300 (PJM). Set `deltaOnly: true` on a scheduled run to be billed only for what actually changed, which is typically a few dozen rows. Failed runs emit nothing and bill nothing.

### Who uses it

- Renewables and storage developers watching competitor projects enter and leave the queue and catching COD slips that free up capacity or signal congestion.
- Land agents and site scouts spotting new projects by county/state near active points of interconnection.
- Energy consultants building cross-ISO capacity and fuel-mix views without scraping each portal.
- Transmission planners and investors tracking queue velocity, withdrawals, and study-phase progression.
- Developers running a weekly automated monitor on a shortlist of ISOs to get alerted the moment a competitor's project is withdrawn or a status milestone changes, without maintaining their own diff logic.

### Use as an MCP tool

A Pay-Per-Result API callable by AI agents through mcp.apify.com. An agent can pull a region's queue, filter by fuel or capacity, and hand rows to a mapping or outreach step.

### FAQ

**How do I track interconnection queue changes?** Run `snapshot` mode (the default) with `deltaOnly: true` — the actor automatically remembers the last run's snapshot (key-value-store key `QUEUE_SNAPSHOT`) and emits only new projects, status changes, and withdrawals since then. No extra input required.

**How do I get queue withdrawal alerts?** Same as above — every run marks projects missing from the latest pull as `row_type: "withdrawn"`, with the project's last-known status and requested COD attached. Schedule the actor daily or weekly and pipe deltaOnly output to your alerting/webhook of choice.

**How do I monitor for new interconnection requests (legacy)?** You can still run `delta` mode on a schedule with a `priorKvKey` for the original manual-diff workflow; each run emits only new / withdrawn / status\_change / cod\_slip rows.

**Which ISOs are covered?** All 7 major US grid operators: SPP, MISO, NYISO, CAISO, PJM, ERCOT, and ISO-NE.

**Do I need an API key?** No. Every source is public and keyless, including PJM (uses PJM's own public-page subscription key) and ERCOT (resolves the current month's public GIS Report automatically).

**How is it billed?** One queue record, one synthetic withdrawn row, one iso\_summary row, or one legacy change record is one dataset item (one Result event). Use `deltaOnly: true` to keep monitoring runs lean and low-cost.

# Actor input Schema

## `isos` (type: `array`):

ISO/RTO codes to pull. Leave empty to pull ALL 7 live sources (SPP, MISO, NYISO, CAISO, PJM, ERCOT, ISO-NE) in one run. All 7 are keyless - no account or API key needed. An unrecognised code now FAILS the run before anything is billed, rather than being silently dropped (which used to fall through to "all seven").

## `mode` (type: `string`):

snapshot = emit the current normalized queue, automatically annotated with monitor fields (is\_new\_since\_last\_run, status\_changed, previous\_status) vs. the actor's own self-managed KV snapshot, plus synthetic withdrawn and per-ISO iso\_summary rows. delta = legacy manual mode: compare against a prior snapshot YOU supply (priorItems/priorKvKey) and emit only change rows (new / withdrawn / status\_change / cod\_slip).

## `deltaOnly` (type: `boolean`):

Snapshot mode only. When true, suppress unchanged queue rows and emit ONLY new/status-changed rows, synthetic withdrawn rows, and one iso\_summary roll-up row per ISO — ideal for a scheduled weekly/daily monitor run that only cares about what changed. When false (default), the full queue is emitted as before, PLUS the same withdrawn/iso\_summary rows as free bonus monitoring signal.

## `priorItems` (type: `array`):

Delta mode: the prior run's unified queue items (the array of records this actor produced before). The diff is computed purely against these. Ignored in snapshot mode.

## `priorKvKey` (type: `string`):

Delta mode alternative to priorItems: a key in this actor's key-value store holding the prior snapshot. When set, the current snapshot is also SAVED under this key so scheduled runs diff automatically against the previous run.

## `maxResults` (type: `integer`):

Maximum number of queue records to fetch across ALL selected ISOs combined. The cap is applied in ISO order, so a low value truncates the last ISOs: any ISO that is cut short or never reached is marked iso\_status=truncated / not\_fetched on its iso\_summary row and is excluded from withdrawn-project detection for that run. Leave at the default to pull the whole federation (~18,200 records).

## Actor input object example

```json
{
  "isos": [
    "SPP"
  ],
  "mode": "snapshot",
  "deltaOnly": false,
  "priorItems": [],
  "maxResults": 100000
}
```

# Actor output Schema

## `results` (type: `string`):

Normalized queue records in the default dataset.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "isos": [
        "SPP"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("malonestar/interconnection-queue-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "isos": ["SPP"] }

# Run the Actor and wait for it to finish
run = client.actor("malonestar/interconnection-queue-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "isos": [
    "SPP"
  ]
}' |
apify call malonestar/interconnection-queue-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=malonestar/interconnection-queue-tracker",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/pkHQu69zDE3jICizB/builds/uZzWV116b4o6g6TIo/openapi.json
