Invalid input no longer FAILS the run (2026-11-04, local, not built or pushed; actor.json version stays 0.1). Input that parse_input rejects (e.g. a placeholder or malformed contactEmail, mode: "weekly", a bad date, a reversed or too-long date range, ciks: ["abc"], by_company without CIKs) 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 SEC request is made, no filing 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
TypeError/AttributeError/OverflowError, exit 91) 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 (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 {} (sample mode).
- Unchanged: sample mode without
contactEmail, SEC blocking, temporary download failures and the existing "Not charged" status rows.
requirements.txt: apify >= 4.0.0, < 5.0.0 pinned to apify >= 4.1, < 4.2.
- Memory unchanged (default 256, min 128, max 1024, no defaultRunOptions). 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, one comment), README.md (contactEmail paragraph and a new "Invalid input" FAQ line), requirements.txt, CHANGELOG.md, tests/test_form_d.py (test_invalid_email_still_fails_clearly becomes test_invalid_email_gives_free_invalid_input_row), tests/test_invalid_input.py (new).
Fixed: a live run whose window listed only amendments (D/A) with includeAmendments off reported reason: "noMatches" in its free status row (the message was already right). It now reports reason: "amendments". The README now lists exactly the 7 reasons the code can emit (noMatches, noNewFilings, amendments, filteredOut, fetchErrors, stoppedBySec, costLimit); a test checks the README against the code.
Console: the dataset overview view shows recordType, status and message, so free status rows are readable there.
Fixed: a live run (contactEmail set) that saved nothing ended with an empty dataset. It now pushes one free status row (recordType: "status", statusRow: true, status "warning"/"error", reason, counts, message starting "Not charged: ...") naming the case: no filings in the window (noMatches, with the index days tried or unknown CIKs), nothing new since the last run (noNewFilings), all filtered out (filteredOut, with checked/filtered/unreadable counts), temporary SEC fetch errors (fetchErrors, "error" when every download failed; retried next run), SEC block (stoppedBySec, "error") or a maximum cost below one filing (costLimit). The status message starts with the same text. Runs that save at least one filing or free parseError row are unchanged.
Fixed: a filing whose download failed for a temporary reason (network error, HTTP 429/5xx after 5 tries, or another unexpected HTTP status) was saved as a free parseError row and remembered as seen, so scheduled runs never fetched it again. It is now skipped, not charged, not remembered, logged as a warning and counted in fetchErrors / fetchErrorAccessions in RUN_SUMMARY; the next run retries it. Truly unreadable filings (no Form D XML anywhere in the folder or full-submission text) are still saved free with parseError.
Sample mode (no contactEmail) now says which filters it ignored: a warning in the log, filtersIgnoredInSampleMode and samplesMatchingFilters in RUN_SUMMARY, and a note in the status message. Samples are still not filtered (the Store prefill's filters match none of the 3 samples).
Docs: the 31 of 221 parseError rows in the offline end-to-end test (and the 1 in the cost-cap test) are injected on purpose by tests/fake_sec.py (every filing with k % 7 == 6 has no XML); they are not parser failures.