# Changelog of Grants.gov Scraper: Federal Grant Opportunities Feed (`fecklesspleb/grants-gov-opportunities`) Actor

- **URL**: https://apify.com/fecklesspleb/grants-gov-opportunities/changelog.md
- **Full Actor documentation**: https://apify.com/fecklesspleb/grants-gov-opportunities.md

## Changelog

### Unreleased: invalid input ends SUCCEEDED with a free status row; 256 MB default memory - 2026-11-04 (local, not built or pushed; actor.json version unchanged: 0.1)

- **Invalid input no longer FAILS the run.** Input that `parse_input` rejects (e.g. `statuses: ["open"]`, `agencies: ["HHS!"]`, `aln: "93866"`, `onlyNewSinceLastRun: "maybe"`, a funding filter with `includeDetails: false`, `maxItems: "abc"`) used to call `Actor.fail` (run FAILED, exit 1, status message = the reason). The run now ends SUCCEEDED (exit 0) with one free status row `{"recordType": "status", "type": "status", "status": "error", "statusRow": true, "message": "Invalid input, nothing was fetched or charged: <reason>", "error": "<reason>", "charged": false, "scrapedAt": ...}` (pushed with no charged event) and the same message as the terminal status message. No request is made, no opportunity is charged and no `RUN_SUMMARY` is saved. The prefix is "nothing was fetched or charged" (not "...and no results were charged") because this Actor's pricing has no start event; the reason texts are unchanged.
  - Wrong-type crashes (uncaught `OverflowError`/`AttributeError`, exit 91, e.g. a number = Infinity or a non-string `onlyNewSinceLastRun`) now give the same row with the reason `a field has the wrong type (<python error>).`
  - A whole input that is not a JSON object (a string, a number, a list of objects) used to crash with `AttributeError` (exit 91); it now gives the same row with the reason `the input must be a JSON object, got <type>.` An empty list or null input still means `{}`.
  - Real upstream and runtime failures are unchanged: the existing "Not charged" source-down row and the "no new items" warning row stay as they were.
- **Memory:** `.actor/actor.json` `defaultMemoryMbytes` 1024 -> 256, `defaultRunOptions.memoryMbytes` 1024 -> 256, `minMemoryMbytes` 256 -> 128 (max stays 2048). Local peak RSS is about 70 MB. The README had no memory or start-event text to change.
- `requirements.txt`: `apify >= 4.0.0, < 5.0.0` pinned to `apify >= 4.1, < 4.2`.
- Pricing, default input, prefill and `exampleRunInput` unchanged.
- Files: `my_actor/main.py` (new `end_invalid_input`, wrong-type wrap around `parse_input`, non-object check), `.actor/actor.json`, `README.md` ("Invalid input" FAQ line), `requirements.txt`, `CHANGELOG.md`, `tests/test_invalid_input.py` (new).

### Unreleased: free status row when nothing is new - 2026-10-31 (actor.json version unchanged: 0.1)

- When only-new is on (`onlyNewSinceLastRun` 'yes', or 'auto' in the default daily mode), Grants.gov answered, at least one opportunity was skipped as already delivered (same `stateKey`) and 0 data rows were saved, the run now pushes one row `{recordType: "status", status: "warning", message: "Not charged: no new items since last run. ...", source, host, skippedSeen, stateKey, checkedAt}` with no charged event (only the free default dataset-item event), logs a warning, mentions it in the status message and sets `statusRow: true` in `RUN_SUMMARY`. Same shape as the source-down row (`status_row()`), with status "warning" instead of "error". Not pushed when data was saved, when the source failed (the source-down "error" row is pushed instead, unchanged), when the cost limit stopped the run, when only-new is off, or when nothing was skipped as seen. This replaces the note in the entry below that runs finding nothing new are unchanged.
- Files: `my_actor/main.py` (new `push_no_new_status()`, called after `push_status_if_empty()` at the end of `run()`), `README.md` ("Nothing new" FAQ line), tests/test_no_new_status.py (new, 5 tests). Tests: 53 (was 48) passed. Pricing, defaults and data rows unchanged.

### Unreleased: cost wording - 2026-10-31 (actor.json version unchanged: 0.1; no code or pricing change)

- Comments and docs no longer say a $0.00005 platform start event is charged: pricing has only the per-item event
  (`grant-opportunity` $0.0015) and no start event. `{}` cost math is per-item only: 6 x $0.0015 = $0.009; a start
  event would apply only if one were configured. Files: `my_actor/main.py` (comment above `DEFAULT_MAX_ITEMS`),
  `pricing.json` (`maxItemsDefaultCap` note), `tests/test_default_cost.py` (docstring; no start event added, limit $0.010),
  and the wording of the older 2026-10-31 entry below. Pricing logic, defaults and charging are unchanged.

### Unreleased: free status row when the source is down - 2026-10-31 (actor.json version unchanged: 0.1)

#### Added: one free `recordType: "status"` row when nothing could be read

A local run with the network down used to finish SUCCEEDED with 0 items and nothing in the dataset, so the user saw an
empty result with no explanation. Now, when Grants.gov cannot be reached or keeps answering with errors (robots.txt
unreadable, DNS/connection failure, repeated 5xx, 403, a non-JSON answer) and 0 data rows were saved, the run pushes one
row `{recordType: "status", status: "error", message: "Not charged: ..."}` with no charged event (only the
free default dataset-item event), logs a warning, mentions it in the status message and sets `statusRow: true` in
`RUN_SUMMARY`. The run still exits 0 / SUCCEEDED. Runs that saved data, and runs that simply found nothing new, are unchanged.

Files changed:

- `my_actor/main.py`: new `status_row()` and `push_status_if_empty()`, called at the end of `run()`; the early-stop
  handler also catches `httpx.HTTPError` and `ValueError` (non-JSON answer) so they stop cleanly instead of crashing.
- `README.md`: new "Source down" FAQ line.
- `tests/test_netdown_status.py`: new (DNS down, HTTP 500, non-JSON; helper is quiet when data was saved or nothing failed).

### 2026-10-31 (actor.json version unchanged: 0.1)

#### Changed: default `maxItems` 500 -> 6 (prefill 100 -> 6)

Ranger's cloud `{}` run of build 0.1.2 stopped at the $0.02 max-charge cap (13 opportunities x $0.0015). `{}`, a
blank `maxItems` and the Console prefill now stop at 6 opportunities x $0.0015 = $0.009 (per-item events only; a start
event would apply only if one is configured), so the Store's empty-input test stays at about $0.01. Raise `maxItems` for a full daily feed.

Files changed:

- `my_actor/main.py`: new `DEFAULT_MAX_ITEMS = 6`, used by `parse_input` (was a literal 500).
- `.actor/input_schema.json`: `maxItems` default 500 -> 6, prefill 100 -> 6, description states the cost.
- `.actor/actor.json`: new `exampleRunInput` (the prefill: posted + forecasted, last 7 days, open only, details on,
  `maxItems` 6). The version is still 0.1.
- `pricing.json`: `estimatedCostExamples.maxItemsDefaultCap` now describes the 6-item default.
- `README.md`: Price section says `{}` and the prefill stop at 6 opportunities, at most $0.009.
- `tests/test_input.py`: `{}` default is 6. New `tests/test_default_cost.py`: code default == schema default ==
  prefill == `exampleRunInput.maxItems` == 6, cost <= $0.010 (per-item only), README states it.
- `CHANGELOG.md`: new file, this entry.
