Railroad Grade Crossing Risk Screener - FRA Accident History avatar

Railroad Grade Crossing Risk Screener - FRA Accident History

Pricing

from $5.50 / 1,000 crossing records

Go to Apify Store
Railroad Grade Crossing Risk Screener - FRA Accident History

Railroad Grade Crossing Risk Screener - FRA Accident History

Screens any US coordinate or geography against the 242,108-crossing FRA/NTAD grade crossing inventory joined to 50 years of FRA Form 57 accident history. Returns warning devices, traffic and train exposure, fatalities and a per-crossing risk score. A zero is a verified negative.

Pricing

from $5.50 / 1,000 crossing records

Rating

0.0

(0)

Developer

Kyle Maloney

Kyle Maloney

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

12 days ago

Last modified

Share

Railroad Grade Crossing Risk Screener — FRA Accident History

Screen any US coordinate, or a whole state / county / city, against the 242,108-crossing FRA National Highway-Rail Crossing Inventory (published by USDOT/BTS in the National Transportation Atlas Database) joined to 50 years of FRA Form 57 highway-rail accident reports — 251,149 incidents running from 1975 to the present.

For every crossing you get the inventory record (warning devices, public/private, at-grade vs grade-separated, traffic and train counts, timetable speed, track layout, operating railroad) plus its actual accident history — how many, how recent, how severe — and an auditable risk score.

The point of the product is the negative. If a crossing next to your site has never had a reported accident, this actor tells you that as a verified fact (

incidents_checked: true
, incidents_count: 0) — and if the FRA source could not be reached, it tells you that instead (incidents_checked: false, incidents_count: null). Those two answers never look the same.


Who it's for

  • Environmental and property due diligence — a rail crossing 300 ft from a site is a noise, vibration, access and liability finding. Phase I ESA and ASTM site-recon workflows already screen pipelines, tanks and floodplains; this is the rail layer.
  • Logistics, industrial and warehouse siting — inbound truck routes that cross an active at-grade crossing with 40 trains a day and crossbucks only are a real operating and insurance cost. exposure_index (AADT × trains/day) is the standard measure.
  • Traffic-safety and municipal engineering — pull every crossing in a county with its device inventory and casualty history to prioritise Section 130 / grade-separation candidates.
  • Insurance and risk underwriting — per-location crossing proximity, protection class and 50-year loss history.
  • Rail, transit and utility route planning — crossing density and protection along a corridor.

Example input

Coordinate mode — screen specific sites:

{
"assets": [
{ "label": "Denver Union Station district", "lat": 39.7392, "lon": -104.9903, "radiusMiles": 1 },
{ "label": "RTD Colorado Station, Denver", "lat": 39.776004, "lon": -104.943325, "radiusMiles": 0.5 }
],
"includeCrossingRows": true
}

Geography mode — every crossing in a county, with accident history:

{ "state": "CO", "county": "DENVER", "includeCrossingRows": true }

Add the individual accident reports (narratives, speeds, casualties, conditions):

{
"assets": [{ "lat": 41.8781, "lon": -87.6298, "radiusMiles": 1 }],
"includeIncidentDetail": true,
"incidentSinceYear": 2005
}

Output

Three record types, all in one dataset, distinguished by record_type:

record_typeOne row perUse it for
asset_summaryscreened siteheadline counts, nearest crossing, radius totals, site risk band
crossinggrade crossingthe full inventory record plus its accident rollup and risk score
incidentFRA Form 57 reportnarrative-level accident detail (opt-in)

173 declared fields. The highlights:

Location & identity — crossing_id (the FRA number, which is also the join key to every other FRA safety product), crossing_distance_miles, crossing_lat / crossing_lon, position_status, street, city_name, county_name, county_fips, state_abbr, fra_inventory_url, fra_accident_url.

What kind of crossing — crossing_position (At Grade / RR Under / RR Over), is_at_grade, crossing_type, is_public, crossing_purpose.

Protection — has_warning_device, warning_device_type, warning_device_class (active / passive / none), warning_device_fields_conflict, whistle_ban, quiet_zone_indicated.

Exposure — annual_average_daily_traffic, aadt_year, total_through_trains_per_day, max_timetable_speed_mph, main_tracks, total_tracks, exposure_index.

Railroad — railroad_code, railroad_name, railroad_division, railroad_subdivision, parent_railroad_code, railroad_milepost, railroad_operation_end_date, operating_railroad_ended.

Accident history — incidents_checked, incidents_count, incidents_fatalities, incidents_injuries, incidents_last_5y, incidents_last_10y, incidents_first_date, incidents_last_date, years_since_last_incident, worst_incident_date, worst_incident_fatalities, hazmat_incidents, pedestrian_incidents, and the full most_recent_incident_* set including the FRA narrative.

Risk — risk_score (0-100), risk_band, risk_score_basis, risk_factors.

Provenance on every row — inventory_source_status, inventory_vintage, incident_source_status, incident_data_updated_at, incident_coverage_end_date, incident_reporting_lag_days, incident_history_complete, data_sources_ok, matched_rows_total, results_truncated.

The live data-integrity gate, on the row — drift_gate_status (verified, or verified_degraded when a corroborating cross-check could not be completed; a fatal outcome never reaches a row at all), drift_probes_verified, drift_probes_unavailable, drift_probes_degraded (the names), drift_gate_note (why, in plain English).


How this actor refuses to give you a confident wrong answer

Every item below is a measured property of the upstream data, not a hypothetical.

A zero is either verified or it is null — never both. incidents_count: 0 means FRA answered and holds no report. If the incident source fails, the count is null, incident_history_complete is false, and risk_score is null — the actor will not publish a "low risk" derived from a query that never returned.

16.3% of the inventory is not at grade. 21,251 crossings are RR Under and 18,157 are RR Over. Counting all 242,108 records as at-grade conflict points overstates real exposure by about a fifth, so is_at_grade ships on every row and the summary splits the counts. A live check on every run fails the actor if the upstream ever stops publishing grade-separated records, because at that moment is_at_grade would silently become a constant true.

A blank warning-device code does not mean "unprotected." WDCODE is blank on 106,597 crossings (44.0%) — and 44,704 of those have SignsOrSignals = Yes, i.e. a device exists and its type was simply not reported. A further 13,610 crossings name a device while SignsOrSignals says No; that contradiction is surfaced in warning_device_fields_conflict rather than silently resolved in one direction.

288 crossings have no usable position. BTS moves a crossing to latitude/longitude 0,0 when its reported coordinate falls outside its own designated state. Those records get position_status: "unlocatable_moved_to_null_island", a null position and a null distance — the raw reported pair still ships in reported_latitude / reported_longitude, but the actor never quietly falls back to the coordinate BTS itself rejected. Radius queries exclude them server-side, so a search near the Gulf of Guinea does not return all 288.

33.7% of crossings publish no traffic count. An unreported AADT is null, never 0, and exposure_index is null rather than a fabricated zero.

The accident file runs about 2.5 months behind. FRA refreshed the dataset today, but the most recent incident in it is from 2026-05-31. Every row publishes incident_coverage_end_date and incident_reporting_lag_days, and every recency window (incidents_last_5y, years_since_last_incident) is anchored to that date rather than to the clock — so a figure cannot drift on its own between runs.

Not every accident joins to a live crossing. In Colorado, 1,302 distinct crossing IDs appear in the accident file but only 932 (71.6%) are still in the current inventory; 23.3% of Colorado incidents therefore cannot be attributed to an open crossing. That is because the crossing was closed, consolidated or re-identified after the accident — or because the ID predates the modern 6-digit-plus-letter format (466D0, 0253087). A further 5,513 incidents nationally (2.2%) carry no crossing ID at all. So a per-crossing count is an honest lower bound on the crossing as it exists today, not a count of everything that ever happened at that spot.

The two-digit year is pivoted from evidence, not assumption. incidentyear is text and two digits. Its distinct set is exactly {75..99} ∪ {00..26} and each band was cross-checked against the file's own date column — 52 of 52 agree. Rule:

yy >= 75 → 19yy
, otherwise 20yy. The band 27-74 is ambiguous and currently empty; a live check fails the run if FRA ever back-loads a value into it, rather than letting a 1974 record be published as 2074.

Nothing is billed unless a live gate passes first. Before any row is written the actor measures both upstreams and checks: inventory count floor, the presence of both at-grade and grade-separated records, the closed CrossingPosition and CrossingType vocabularies, the full field contract, inventory freshness, incident count floor, incident freshness, century-pivot integrity, a positive canary (crossing 945876D at Colorado Station in Denver, and its 2026-05-30 fatality, which also proves the crossing-ID join is still intact), and a negative control (a 50-mile radius in the open Pacific must return zero). Any failure ends the run with an explanatory message and zero rows, zero charges.

"We could not ask" and "the answer changed" are different things, and the actor tells them apart. ArcGIS Online delivers a momentary overload and a genuine schema change through the same HTTP 200 response carrying an embedded error envelope. Measured on 2026-09-19: firing 40 concurrent queries at the crossing layer produced one refusal after 55 seconds while the other 39 succeeded, and the byte-identical query answered in 12 ms immediately afterwards — the refusal named no parameter, which is how a load-shed refusal differs from a real one ('outFields' parameter is invalid, Invalid URL), and those repeat deterministically in well under a second. So:

  • a refusal that names nothing is treated as availability, retried, and normally absorbed without you ever seeing it;
  • a refusal that names what is wrong is re-asked exactly once — a deterministic refusal repeats — and then fails the run loudly as drift;
  • an answer that arrives and is measured wrong is never retried and always fatal.

A verified inventory is still a saleable answer when the accident file is unreachable. Every probe carries a role. The NTAD inventory probes are load-bearing: if any of them cannot be confirmed the run fails and bills nothing. The FRA Form 57 probes are corroborating: if that source cannot be reached, the run proceeds on the verified inventory, every incident count, fatality/injury total and risk_score is published as null (meaning not checked — never 0), incident_history_complete is false, and drift_gate_status reads verified_degraded with drift_probes_degraded and drift_gate_note naming exactly which cross-check did not run. An FRA probe that answers and is wrong still fails the run — that is drift, not an outage.

Waiting is bounded, and the bound is measured. Every gate probe shares one 150-second wall clock and each attempt is clamped to the time actually left, so a retrying upstream can never turn a screen into a run that never ends. Running out of time is treated as "we could not ask", not as "the answer changed". The budget is sized from the worst single upstream request actually observed (55,134 ms on 2026-09-19, one of 40 concurrent queries) rather than from a round number, and a test fails if it is ever tightened below that.

You can prove all of this yourself. simulateOutage drives one named failure path end to end on the live build: transient_canary_refusal (ArcGIS refuses twice then answers — the run should still succeed), persistent_canary_refusal and named_parameter_refusal (load-bearing failure — zero rows, zero charges), canary_field_drift (the canary answers with a wrong value — still fatal), incident_outage (the accident file vanishes — the run degrades rather than failing). Leave it at none for normal use.

A partial read is an error, not a result. Crossing retrieval is paged to completion and then reconciled against the service's own count; a shortfall aborts rather than publishing an arbitrary subset. If you cap the result with maxCrossings, matched_rows_total still reports the true total and results_truncated is set.


Notes and limits

  • risk_score is editorial. FRA publishes no official per-crossing risk rating in this dataset. risk_score_basis names the model and risk_factors itemises every contribution so you can audit or re-weight it. It is a screening aid, not a regulatory determination.
  • reason_code (FRA Form 71 reason-for-update) ships verbatim with
    reason_code_label: null
    and reason_code_label_basis: "unmapped_fra_reason_code". FRA ships no decode table inside the NTAD extract, and a guessed label across 242,108 rows would be worse than no label.
  • Geography mode matches the FRA spelling exactly: uppercase, no "County" suffix (state: "CO", county: "DENVER").
  • Coverage is the 50 states, DC and territories present in the FRA inventory. There is no crossing data for locations outside it, and a radius there correctly returns zero.
  • An empty run input fails loudly with an actionable message and bills nothing.

Use as an MCP tool

This actor is callable by AI agents through Apify's MCP server at https://mcp.apify.com. Billing is unchanged when it is called as a tool, and a run that cannot answer fails without billing.

{
"mcpServers": {
"apify": {
"command": "npx",
"args": ["-y", "@apify/actors-mcp-server", "--actors", "malonestar/ntad-fra-rail-crossing-risk-screener"],
"env": { "APIFY_TOKEN": "your-apify-token" }
}
}
}

Worked agent prompt:

"Screen 1200 W 47th Ave, Denver CO for railroad exposure. Give me every grade crossing within a mile, whether each is at grade, what warning devices it has, and its FRA accident history — and tell me explicitly which crossings were verified to have no accidents versus which could not be checked."

Chains naturally with the rest of the site-diligence shelf — pipelines, tanks, floodplain, wetlands, contaminated sites — since all of them screen the same coordinate.

FAQ

Where does the crossing data come from? The FRA National Highway-Rail Crossing Inventory, published as the USDOT/BTS NTAD "Railroad Grade Crossings" layer. The vintage is read from the service on every run and published as inventory_vintage.

Where does the accident data come from? FRA "Highway-Rail Grade Crossing Incident Data (Form 57)", the official railroad-reported accident file, 1975 to present.

Does a crossing with zero incidents really mean zero? It means FRA holds no Form 57 report joined to that crossing ID through incident_coverage_end_date. Read the caveats on join rates above — and check incidents_checked before trusting any zero.

Can I get every crossing in a state? Yes — { "state": "TX" }. Use maxCrossings to bound the cost; matched_rows_total always reports the true total.

How is this priced? Pay per result row. An asset_summary row plus one row per crossing found; incident rows only if you ask for them.