← Guides

How-to

How to run OSHA Violation Risk & Time-Series

Step-by-step: the exact input, what each field does, and the real rows that come back from OSHA IMIS.

You have a list of employers and you need to know what OSHA has on each of them. The data is already public: OSHA’s IMIS establishment search will show you every inspection it has recorded for a company name, and clicking into an inspection shows each citation, its gravity, its standard, and the penalty. Nothing is hidden and nothing costs money. What it doesn’t do is add anything up. There is no total, no per-year series, no flag for repeat citations, and no way to ask the same question about twenty companies without doing it twenty times, one search and one detail page at a time, pasting numbers into a spreadsheet as you go. This actor does that search, opens each inspection, and returns the arithmetic already done — per employer, in one row you can sort.

What you’ll need

Nothing but an Apify account. The actor talks to OSHA IMIS directly.

No API key, no CAPTCHA solver, no proxy. OSHA’s IMIS pages are plain public HTML with no anti-bot wall in front of them, so the actor fetches them straight. There’s a proxyConfiguration field in the input, but it’s off by default and you almost certainly don’t need it.

Step 1 — Open the actor

Open OSHA Violation Risk & Time-Series on the Apify Store and press Try for free. The actor’s own page on this site lists its live health and pricing.

The OSHA Violation Risk & Time-Series listing on the Apify Store
The listing on the Apify Store. Pricing and the nightly health record are both public.

Step 2 — Fill in the input

The input form for OSHA Violation Risk & Time-Series in the Apify Console
The input form, as it appears in the Apify Console.
FieldTypeRequiredWhat it does
employersarrayyesUS employer / company names to analyze (e.g. ‘Dollar General’). One risk report per name, aggregating all of that employer’s OSHA inspections.
statestringnoOptional two-letter state code (e.g. ‘TX’) to limit inspections to that state. Leave empty for nationwide.
maxInspectionsPerEmployerintegernoCap on inspections pulled per employer (most-recent first). Each inspection is one detail fetch; higher = more complete history, slower run. (default 25)
compareToPreviousbooleannoWhen on (default), diff each employer against the previous run and report what changed — new inspections, new violations, risk-score delta. Run on a schedule to track employers… (default True)

Two fields decide what you actually get back.

employers is a text search against OSHA’s establishment names, not a company identifier. OSHA records the establishment as it was written on each inspection — store numbers, abbreviations, state prefixes — so a short, canonical name (Dollar General) matches broadly, and a long or punctuated one may match almost nothing. Whatever it matches is aggregated into a single company-level report. Read the establishments array in the output to see exactly which names got folded in, and add the state filter if you’re pulling in worksites you didn’t mean.

maxInspectionsPerEmployer (default 25, max 200) is a cap on the most recent inspections pulled, and it is not just a speed knob: the risk score, the totals, and the time-series are computed over the inspections that were pulled. Cap a large national employer at 25 and you get the risk of its 25 most recent inspections, not of its whole history. For a small vendor, 25 is likely all there is. For a national chain, raise it — and keep it the same across employers you intend to compare.

A working input:

{
  "employers": [
    "Dollar General"
  ],
  "maxInspectionsPerEmployer": 25,
  "compareToPrevious": true
}

Step 3 — Run it

Press Start. Rows are pushed to the dataset as they’re found, so the run log fills in as it works.

You get exactly one row per name in employers — three names in, three rows out. The work behind each row is one search request plus one detail request per inspection, so the run length is driven almost entirely by maxInspectionsPerEmployer times the number of employers. That’s the honest shape of it; the actual wall-clock depends on OSHA’s servers on the day, so run it once and look.

A row can come back with inspection_count: 0. That is a real answer, not a failure — either OSHA has no inspections recorded under that name, or the name you typed doesn’t match how OSHA wrote it (try the shorter, plainer form of the company name, and clear the state filter). Empty employers are still returned so you can see them, and they are not charged.

Step 4 — Read the output

Each run returns a labelled table, not raw JSON:

The output fields returned by OSHA Violation Risk & Time-Series
The output view — every field the actor returns, named and typed.

Every row looks like this — a real row from a real run:

{
  "query": "Dollar General",
  "state": null,
  "establishments": [
    "Dollar General Store 12345"
  ],
  "inspection_count": 2,
  "violation_count": 2,
  "by_gravity": {
    "serious": 2
  },
  "total_current_penalty": 14000,
  "total_initial_penalty": 14000,
  "risk_score": 21,
  "risk_band": "medium",
  "repeat_offender": false,
  "trend": "falling",
  "violation_time_series": [
    {
      "year": 2024,
      "violations": 2,
      "penalty": 14000
    }
  ],
  "first_seen": false,
  "new_inspections": 0,
  "new_violations": 0,
  "risk_score_delta": 0,
  "source_url": "https://www.osha.gov/ords/imis/establishment.search?establishment=Dollar+General&state=all"
}
FieldExample valueMeaning
queryDollar GeneralThe employer name you searched
risk_score21Deterministic 0-100 severity-weighted, recency-decayed risk score
risk_bandmediumlow / medium / high
repeat_offenderFalseTrue if any Repeat-gravity violations
trendfallingrising / flat / falling (recent vs prior 2-year windows)
by_gravity{"serious": 2}Violation counts by OSHA gravity (willful/repeat/serious/other)
total_current_penalty14000Sum of current penalties (USD)
violation_time_series[{"year": 2024, "violations": 2, "penalty": 14000}]Violations + penalty per year

The field people come for is risk_score, so be precise about what it is. It is our arithmetic over OSHA’s public record — not an OSHA rating, not an OSHA endorsement, and not a prediction. OSHA does not publish a score; this one is computed here, deterministically, and the formula is short enough to print:

  • Every citation is weighted by the gravity OSHA assigned it: Willful 10, Failure to Abate 8, Repeat 6, Serious 3, Unclassified 2, Other 1.
  • Each weight is multiplied by a recency factor: full weight if the citation was issued within the last two years, then decaying by 0.1 per further year, never below 0.3. A citation with no readable issuance date counts at half weight and is left out of the series and the trend.
  • Those decayed weights are summed, and the sum is mapped onto 0–100 with a saturating curve (100 × (1 − e^(−sum / 25))), so the score climbs steeply at first and then flattens — it approaches 100 without ever getting there.
  • risk_band is a cut on that number: 50 and up is high, 20 and up is medium, below that low.
  • Penalty totals are reported next to the score but are not folded into it. Penalty amounts move with era, region, and settlement; gravity is the stable signal.
  • repeat_offender is not part of the score either. It is a plain fact read off the record: true if at least one citation is classified Repeat.
  • trend compares the gravity-weighted violations of the last two years against the two years before that — rising above 1.2×, falling below 0.8×, flat in between.

The same input history always produces the same score, so you can hand it to someone and they can check it. What it cannot do is see hazards nobody inspected: it scores the record, not the workplace. And the counts are counts — a chain with two thousand stores accumulates inspections a single-site employer never will, so a raw score comparison between companies of very different size is not a like-for-like one.

Two things to watch in the columns themselves. by_gravity only contains the keys that actually appeared, so a clean employer’s is {} rather than a row of zeroes — read a missing key as zero. And establishments is OSHA’s spelling, not yours: expect store numbers and inconsistent capitalisation in it, and use it to sanity-check that the name matched what you meant. On a first run first_seen is true and new_inspections, new_violations and risk_score_delta are all null; they only carry meaning from the second scheduled run onward.

Step 5 — Export it

Open the Dataset tab and export to CSV, JSON, or Excel — or pull the same rows from the API, which is what you want if this is going to run on a schedule.

What it costs

$0.02 per employer risk report. One charge per employer you get a report for — 100 employers is $2.00. An employer with no OSHA inspections found is returned as an empty row and is not charged, so a misspelled name costs you nothing but the run. Apify’s platform compute is billed separately, per your plan.

The data itself is free: OSHA publishes all of it, and if you only need one employer you should just go and look. What you’re paying for is the rollup and the repetition — the score, the time-series, and the fact that a list of forty suppliers is one run rather than a morning.

Where the data comes from

This reads OSHA IMIS directly. The target is HTML parse. Every night a canary runs this actor against that live source and diffs the result against a frozen fixture — what “verified” means.

IMIS is OSHA’s enforcement record — inspections and the citations issued from them — and it is genuinely open: no login, no key, no anti-bot wall. The annoyance is its shape. The search page gives you a list of inspection IDs; the citations, gravities and penalties live one page deeper, so a company with fifty inspections is fifty more clicks before you have anything to add up. The establishment name is recorded per inspection rather than per company, so the same employer appears under a dozen spellings and nothing joins them for you. And the record is a record: it lags, it reflects only workplaces that were actually inspected, and an inspection appearing in it is not a finding of guilt — citations can be, and are, contested and reduced.

See it used

If your question is about a contractor rather than an employer’s safety record, this is the wrong tool: California Contractor License Lookup tells you whether a licence is live, bonded and insured. The two pair naturally — verify the licence there, then check the safety record here.

Last updated 2026-07-13