# Nursing Home Daily Nurse Staffing (CMS PBJ) Scraper (`jserle/cms-nursing-home-staffing-daily`) Actor

Nursing home staffing data from the CMS Payroll Based Journal (PBJ): daily RN, LPN and CNA hours per facility, employee vs contract, MDS census and hours per resident day. Filter by CCN, state, county, facility name or date. About 1.3M facility-days per quarter. $0.002/record.

- **URL**: https://apify.com/jserle/cms-nursing-home-staffing-daily.md
- **Developed by:** [Jonathan Serle](https://apify.com/jserle) (community)
- **Categories:** Lead generation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 staffing day records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Nursing Home Daily Nurse Staffing (CMS PBJ) Scraper

Pulls CMS Payroll Based Journal (PBJ) daily nurse staffing data for every US nursing home into clean JSON records: one item per facility per calendar day, with RN, LPN and CNA hours split into employee and contract (agency) hours, the MDS resident census, and hours per resident day already computed. Filter by CCN, state, county, facility name or work date.

The PBJ is the most granular staffing record that exists for the sector, but CMS publishes it as quarterly CSV files of well over a million rows and a paginated API with no text search. This Actor lets you pull one facility's full daily time series, or every facility in a county, without downloading the quarter.

About 1.3 million facility-days per quarter, covering roughly 14,500 facilities.

### Who uses this data

- **Nursing home abuse attorneys and LTC liability analysts** building staffing-risk evidence. Pull one facility's full daily time series by CCN and you have the day-by-day record of whether it was staffed, including the days it leaned on agency contractors.
- **SNF operators and chains benchmarking** against their county or state. Compare your hours per resident day against every competitor in the same market, on the same days.
- **Nursing home lenders, brokers and underwriters** reading contract-labour dependence and census trends before pricing a facility.
- **Healthcare staffing agencies** sizing agency demand by market: `contractHoursShare` shows which facilities run on contract labour.
- **Researchers, journalists and policy teams** studying staffing levels, weekend staffing dips, or census trends.
- **Anyone joining staffing to outcomes.** Records carry the CCN, so they join directly to CMS Care Compare, inspection, and quality datasets.

### What you get

One dataset item per facility per day, ordered by provider number then work date, so each facility's days arrive contiguous and in sequence.

```json
{
  "id": "015009-2026-01-01",
  "providerCcn": "015009",
  "providerName": "BURNS NURSING HOME, INC.",
  "city": "RUSSELLVILLE",
  "state": "AL",
  "county": "Franklin",
  "countyFips": "059",
  "calendarQuarter": "2026Q1",
  "workDate": "2026-01-01",
  "mdsCensus": 52,
  "hrsRnDon": 0,
  "hrsRnDonEmployee": 0,
  "hrsRnDonContract": 0,
  "hrsRnAdmin": 6.4,
  "hrsRnAdminEmployee": 6.4,
  "hrsRnAdminContract": 0,
  "hrsRn": 42.28,
  "hrsRnEmployee": 42.28,
  "hrsRnContract": 0,
  "hrsLpnAdmin": 0,
  "hrsLpn": 31.61,
  "hrsLpnEmployee": 31.61,
  "hrsLpnContract": 0,
  "hrsCna": 138.06,
  "hrsCnaEmployee": 138.06,
  "hrsCnaContract": 0,
  "hrsNurseAideTraining": 0,
  "hrsMedAide": 0,
  "totalRnHours": 48.68,
  "totalNurseHours": 218.35,
  "totalContractHours": 0,
  "totalRnHprd": 0.9362,
  "totalNurseHprd": 4.199,
  "contractHoursShare": 0,
  "source": "https://data.cms.gov/data-api/v1/dataset/7e0d53ba-8f02-4c66-98a5-14a1c997c50d/data",
  "attribution": "Centers for Medicare & Medicaid Services (CMS)",
  "fetchedAt": "2026-08-23T09:12:44.201Z"
}
```

Trimmed for length: every one of the eight staff categories carries a total plus an `...Employee` and `...Contract` split, so the full item also has `hrsLpnAdminEmployee`, `hrsLpnAdminContract`, `hrsNurseAideTrainingEmployee`, `hrsNurseAideTrainingContract`, `hrsMedAideEmployee` and `hrsMedAideContract`.

The eight categories are the ones CMS reports: RN Director of Nursing (`hrsRnDon`), RN with administrative duties (`hrsRnAdmin`), RN (`hrsRn`), LPN with administrative duties (`hrsLpnAdmin`), LPN (`hrsLpn`), CNA (`hrsCna`), nurse aide in training (`hrsNurseAideTraining`) and med aide or technician (`hrsMedAide`).

**Six fields are computed by this Actor**, not published by CMS:

- `totalRnHours` = `hrsRnDon` + `hrsRnAdmin` + `hrsRn`
- `totalNurseHours` = `totalRnHours` + `hrsLpnAdmin` + `hrsLpn` + `hrsCna`
- `totalContractHours` = the eight `...Contract` fields added up
- `totalRnHprd` = `totalRnHours` / `mdsCensus`
- `totalNurseHprd` = `totalNurseHours` / `mdsCensus`
- `contractHoursShare` = `totalContractHours` / all reported hours

These are plain arithmetic over the published columns. They are **not** the case-mix adjusted staffing measures CMS calculates for the Five-Star rating, and should not be compared against a facility's published star rating. When the census is zero the two per-resident-day fields are `null` rather than infinity.

Hours arrive as numbers, not `"42.28"` strings. Dates are ISO 8601; the source publishes `20260101`, this Actor delivers `2026-01-01`. A value CMS left blank is `null`, never an empty string or a guessed zero. Records are deduplicated on `id`, which is the CCN plus the work date.

### Input

Everything is optional. Run it with no input and you get 100 facility-days from the latest published quarter.

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `providerCcns` | string\[] | - | CMS Certification Numbers (`PROVNUM`). `15009` and `015009` both work. Up to 100 per run. |
| `state` | string | - | Two-letter postal abbreviation, e.g. `NY`. Case-insensitive. |
| `county` | string | - | County name as CMS spells it, without the word "County", e.g. `Westchester`. Pair it with `state`. |
| `facilityNameKeyword` | string | - | Case-insensitive substring of the facility name. **Requires another filter alongside it**; see below. |
| `workDateFrom` | ISO date | - | Keep days on or after this date, e.g. `2026-02-01`. |
| `workDateTo` | ISO date | - | Keep days on or before this date. |
| `maxResults` | integer | 100 | 1 to 50000. This is your cost cap: you pay per delivered record. |
| `includeRaw` | boolean | false | Attach a `raw` object with the CMS row exactly as the API returned it. |
| `allowPartialScan` | boolean | false | Accept a partial sample instead of failing when the page budget runs out. See below. |

Filters combine with AND. `providerCcns`, `state`, `county` and the date range are pushed down to the CMS API, so a narrow query reads only the rows it needs.

`facilityNameKeyword` is the exception: the CMS API has no text search, so the name is matched after fetching. It therefore **cannot be used on its own** and the run is rejected before making a single request unless you pair it with at least one of `state`, `county`, `providerCcns`, `workDateFrom` or `workDateTo`. Paired, it reads at most 10 pages (50,000 rows) of that already-narrowed table.

#### Example inputs

**One facility's full daily staffing record for the quarter** (litigation, diligence):

```json
{
  "providerCcns": ["015009"],
  "maxResults": 100
}
```

**Every facility in one county for one month** (operator benchmarking):

```json
{
  "state": "NY",
  "county": "Westchester",
  "workDateFrom": "2026-03-01",
  "workDateTo": "2026-03-31",
  "maxResults": 2000
}
```

**A chain's facilities in one state** (name keyword paired with a state filter, as required):

```json
{
  "state": "PA",
  "facilityNameKeyword": "Genesis",
  "workDateFrom": "2026-03-01",
  "maxResults": 5000
}
```

#### When a run fails rather than returning less

Two situations produce a failed run, not a thin one. Both exist because a short answer that looks complete is worse than no answer.

- **The page budget ran out before `maxResults` was met.** A run reads at most 40 pages, or 10 when a name keyword is in play. If it hits that ceiling with rows still unread, it fails and tells you to narrow the query or lower `maxResults`. Set `allowPartialScan: true` to take the partial sample instead; the run summary then reports `scanTruncated: true` so you can tell the difference. Running out of *matching rows* is not this: delivering 30 records when you asked for 100 because only 30 exist is a complete, successful answer.
- **An unfiltered run delivered nothing.** If you supply no filters at all and CMS reports a full table but no records come back, the source or its response shape is broken, and the run fails instead of reporting an empty success. A *filtered* run that matches nothing is a valid, free, successful result.

### Pricing

**$0.002 per record; you pay only for records delivered.** One charge per dataset item, nothing else. Set `maxResults` and you have set your bill: 1,000 facility-days is $2.00. Filters are applied before delivery, so a query that matches nothing costs nothing.

### Data source, refresh and licence

Data comes from the Centers for Medicare & Medicaid Services, through the public data.cms.gov Data API:

- Dataset: [Payroll Based Journal Daily Nurse Staffing](https://data.cms.gov/quality-of-care/payroll-based-journal-daily-nurse-staffing)
- Endpoint: `https://data.cms.gov/data-api/v1/dataset/7e0d53ba-8f02-4c66-98a5-14a1c997c50d/data`
- [Data dictionary](https://data.cms.gov/resources/payroll-based-journal-daily-nurse-staffing-data-dictionary) and [methodology](https://data.cms.gov/resources/payroll-based-journal-methodology-0)

**Refresh:** CMS publishes this dataset quarterly. Its catalog entry gives `accrualPeriodicity` as `R/P3M`, and the distribution read here was last modified 2026-07-29 covering 2026-01-01 to 2026-03-31. Each run reports the row count it saw in the run summary under the `OUTPUT` key.

**Licence:** CMS's own catalog entry for this dataset (`https://data.cms.gov/data.json`) records `"accessLevel": "public"` and `"license": "https://www.usa.gov/government-works"`.

**Attribution:** Centers for Medicare & Medicaid Services (CMS). This Actor is not affiliated with or endorsed by CMS or the US Department of Health and Human Services.

`data.cms.gov/robots.txt` sets `crawl-delay: 10` and disallows only `/data-api/v1/download-request` and `/data-api/v1/tracker/dataset/dataset_views/` under the API path; the `/data-api/v1/dataset/<id>/data` path used here is allowed. The Actor honours the 10 second delay between every request, sends a descriptive User-Agent, opens no parallel connections, and caches nothing between runs. A default 100-record run makes two requests.

### Limitations

- **One quarter at a time.** The endpoint this Actor reads is the distribution CMS labels "latest", which holds the most recent published quarter and nothing else. A date range outside that quarter returns no records. The Actor reports the quarter it read in every item's `calendarQuarter`, and CMS keeps earlier quarters as separate downloads on the dataset page.
- **Nurse staffing only.** CMS publishes non-nurse staffing (respiratory therapists, occupational therapists, social workers) as a separate dataset. This Actor covers the nurse staffing file.
- **`facilityNameKeyword` cannot be pushed down.** The CMS API has no text search and silently ignores `like` and `contains` operators, so a name keyword is matched after fetching. That is why it must be paired with a filter the API can apply, and why its scan is capped at 10 pages (50,000 rows) of the narrowed table. Search a chain within a state or a date range, not across the whole country.
- **Reported hours, not scheduled or required hours.** These are hours the facility reported paying for and CMS accepted. Days a facility failed to submit are absent rather than zero. A zero-hour day with a real census is what CMS published.
- **Census is the MDS census**, calculated by CMS from Minimum Data Set submissions, not a facility's own headcount. It is zero on 191 facility-days in the current quarter, which is why the per-resident-day fields can be `null`.
- **Data is as published.** CMS revises and republishes quarters; a record reflects what the API served at `fetchedAt`.

### FAQ

#### What is the Payroll Based Journal (PBJ)?

CMS's mandatory staffing submission for nursing homes. Every certified facility reports the hours it paid for, by staff category and by day, from payroll records. CMS publishes the accepted submissions quarterly, and this Actor reads the nurse staffing file.

#### How do I get nursing home staffing data for one facility?

Put its CCN in `providerCcns`. You get one record per calendar day in the published quarter, in date order, with the hours by category and the computed hours per resident day.

#### Does it show agency or contract nurse staffing?

Yes. Every category carries an `...Employee` and a `...Contract` split, and `totalContractHours` and `contractHoursShare` summarise them per day.

#### How do I calculate nursing home hours per resident day (HPRD)?

`totalNurseHprd` and `totalRnHprd` are already on every record: total nurse hours (RN, LPN and CNA categories) and total RN hours divided by that day's MDS census. These are plain arithmetic, not the case-mix adjusted measures behind the CMS staffing star rating.

#### How many quarters of PBJ data can I pull?

One: the most recent quarter CMS has published (`calendarQuarter` on every record). The copy read on 2026-08-23 covered 2026-01-01 to 2026-03-31.

#### Can I search by facility name?

Yes, with `facilityNameKeyword`, but only alongside a state, county, CCN or date filter, because the CMS API has no text search and the name is matched after fetching.

### Related datasets

Other datasets by the same author, all pay-per-record with source, refresh cadence and licence stated on each listing:

**Nursing home and long-term care data**

- [Nursing Home Deficiency Citations (CMS) Scraper](https://apify.com/jserle/nursing-home-deficiency-citations)
- [Nursing Home Fines & Civil Money Penalties (CMS) Scraper](https://apify.com/jserle/nursing-home-penalties-fines)
- [Nursing Home Directory & Ownership (CMS Form 671) Scraper](https://apify.com/jserle/ltc-facility-characteristics)
- [Nursing Home Change of Ownership (CMS SNF CHOW) Scraper](https://apify.com/jserle/snf-change-of-ownership-tracker)
- [New York Nursing Home Bed Availability (NYSDOH) Scraper](https://apify.com/jserle/ny-nursing-home-bed-census)

**Medicare provider enrollment data**

- [Medicare Revoked Providers List (CMS Revocations) Scraper](https://apify.com/jserle/medicare-revoked-providers)
- [Medicare Opt-Out Providers List (CMS Affidavits) Scraper](https://apify.com/jserle/medicare-opt-out-physicians)
- [Medicare Revalidation Due Date List (CMS) Scraper](https://apify.com/jserle/medicare-revalidation-due-leads)

**Public procurement and RFP feeds**

- [E-Rate Form 470 RFP Scraper (USAC School & Library Bids)](https://apify.com/jserle/erate-form-470-rfp-feed)
- [NYC PASSPort Procurement Scraper (RFPs, Bids & Contracts)](https://apify.com/jserle/nyc-passport-procurement)

**Developer and AI test data**

- [Synthetic Invoice OCR Ground Truth Dataset Generator](https://apify.com/jserle/ocr-ground-truth-document-factory)

# Actor input Schema

## `providerCcns` (type: `array`):

One or more six-digit CMS Certification Numbers, the `PROVNUM` column. A number that lost its leading zero in a spreadsheet is padded back out, so 15009 and 015009 both work. Up to 100 per run. Example: 015009

## `state` (type: `string`):

Two-letter postal abbreviation, matched case-insensitively. Example: NY

## `county` (type: `string`):

County name exactly as CMS spells it, matched case-insensitively and without the word "County". Pair it with a state, because county names repeat across states. Example: Westchester

## `facilityNameKeyword` (type: `string`):

Case-insensitive substring of the facility name, useful for finding a chain. The CMS API has no text search, so this is matched after fetching and REQUIRES at least one of state, county, providerCcns, workDateFrom or workDateTo alongside it. The keyword scan reads at most 10 pages (50,000 rows). Example: GENESIS

## `workDateFrom` (type: `string`):

ISO date, for example 2026-02-01. Keeps days on or after this date. The dataset covers one calendar quarter, so a range outside it returns nothing.

## `workDateTo` (type: `string`):

ISO date, for example 2026-02-28. Keeps days on or before this date.

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

How many facility-day records to deliver. You are charged per delivered record, so this is also your cost cap.

## `allowPartialScan` (type: `boolean`):

By default a run FAILS if it hits its page budget before delivering `maxResults`, because a truncated scan is an incomplete answer that would otherwise look complete. Turn this on to accept a best-effort partial sample instead; the run summary reports `scanTruncated: true`.

## `includeRaw` (type: `boolean`):

Attach a `raw` object holding the CMS row exactly as the API returned it, under the original column names and as strings. Useful for auditing the normalization; roughly doubles item size.

## Actor input object example

```json
{
  "providerCcns": [],
  "maxResults": 100,
  "allowPartialScan": false,
  "includeRaw": false
}
```

# Actor output Schema

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

One item per facility-day delivered by this run.

## `summary` (type: `string`):

Source dataset, row counts, how much of the source was scanned, delivered records and whether the charge limit or the scan cap stopped the run.

# 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 = {
    "providerCcns": [],
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("jserle/cms-nursing-home-staffing-daily").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 = {
    "providerCcns": [],
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("jserle/cms-nursing-home-staffing-daily").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "providerCcns": [],
  "maxResults": 100
}' |
apify call jserle/cms-nursing-home-staffing-daily --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jserle/cms-nursing-home-staffing-daily"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

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