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
12 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.
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 → 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.
"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_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.