external_id, or as a single ZIP archive that Zenrows builds for you.
Uses: Batch results (GET /jobs/{id}/results) and bulk export (POST /jobs/{id}/runs/{run_id}/exports)
Ask your AI agent
Paste this prompt into a coding agent such as Claude Code or Cursor, with the Zenrows docs connected. The agent looks up the details and writes the calls.Pick a method
Save every result to a folder
Both SDKs page through the results and download each body for you. The CLI does the same and also writes a_manifest.jsonl index with each task’s external_id, metadata, url and file name.
job.run is the job’s latest run. To save another run, use client.run(job_id, run_id). Over the raw REST API, the loop in Scrape a list of URLs does the same: page through GET /jobs/{job_id}/results with cursor and download each result_url.
The CLI run on a job with three successful tasks:
Folder
type: .md for markdown, .json for CSS-extracted fields, .html for pages. The SDKs name files differently: the Python SDK adds the result type as the extension (hoodie-markdown.markdown, capri-fields.json), and the Node.js SDK adds none (hoodie-markdown).
Export the whole run as a ZIP
- Read the run’s
run_idfromlatest_runonGET /jobs/{job_id}. POST /jobs/{job_id}/runs/{run_id}/exportsstarts the export and returns anexport_id.- Poll
GET /jobs/{job_id}/runs/{run_id}/exports/{export_id}untilstatusiscompleted, then downloaddownload_url.
Example output
The Python (Requests) run. Inside the ZIP, files are named bytask_id with an extension for the result type:
GET /jobs/{job_id}/results?status=successful) and join on task_id, or use the CLI’s _manifest.jsonl.
Customize it
Troubleshooting
- Fewer files than tasks: the folder download saves successful results. Read the failures with
status=failed, see Retry the tasks that failed. - A
result_urlordownload_urlreturns403: the presigned link is stale. List the results or poll the export again for a fresh link. - The export stays
pendingorrunning: Zenrows is still building the archive. Keep polling every few seconds. - The files are named by
task_id: passuse_external_id=True(useExternalId: true), and give each task anexternal_idwhen you submit it.
Related recipes
Scrape a list of URLs
Submit thousands of URLs as one Batch job.
Start a Batch job from a CSV file
Upload a whole CSV of URLs at once.
Get notified when a Batch job finishes
Receive a webhook instead of polling.
Retry the tasks that failed
Rerun only the failures and keep the successes.