Railroad Grade Crossing Risk Screener - FRA Accident History
Pricing
from $5.50 / 1,000 crossing records
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
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: trueincidents_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_type | One row per | Use it for |
|---|---|---|
asset_summary | screened site | headline counts, nearest crossing, radius totals, site risk band |
crossing | grade crossing | the full inventory record plus its accident rollup and risk score |
incident | FRA Form 57 report | narrative-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.
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 → 19yy20yy. 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.
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_scoreis editorial. FRA publishes no official per-crossing risk rating in this dataset.risk_score_basisnames the model andrisk_factorsitemises 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 withandreason_code_label: nullreason_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.