> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zenrows.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The Zenrows API (Fetch, Extract via extract=auto, and Batch) is described in OpenAPI 3.1 at https://www.zenrows.com/openapi.json and listed in the RFC 9727 API catalog at https://www.zenrows.com/.well-known/api-catalog. Use the spec for exact parameter names and types.

# Run a Batch job on a schedule

> Scrape the same pages every few hours, daily, weekly or monthly with a scheduled Zenrows Batch job, and read each run's results.

A scheduled job stores your URLs and scrape settings once, and Zenrows runs them on your schedule: every few hours, at set times on chosen days, or once at a future date. Each fire creates a new run with its own results, so you get fresh prices, listings or pages with no cron server to keep alive.

**Uses:** [Batch scheduled jobs](/batch/developer-guide-restapi#scheduled-jobs) (`"type": "scheduled"` with `rate`, `calendar` or `at`), `PUT /jobs/{id}/schedule` and `POST /jobs/{id}/schedule/state`

## Ask your AI agent

Paste this prompt into a coding agent such as Claude Code or Cursor, with the [Zenrows docs connected](/academy/connect-your-ai-agent#give-your-agent-the-zenrows-docs). The agent looks up the details and writes the calls.

```text theme={"dark"}
Using the Zenrows docs, create a Zenrows Batch job that scrapes the product list at
https://www.scrapingcourse.com/ecommerce/ every weekday at 8:00 and 20:00 New York time.
Then write a script that reads the latest run and prints each product's name and price.
```

## Create a scheduled job

Set `"type": "scheduled"` and add a `schedule`. This one runs at 09:00 every day, New York time, and extracts the catalog page as JSON:

<CodeGroup>
  ```python Python (SDK) theme={"dark"}
  # pip install zenrows
  from zenrows import ZenRowsBatchClient
  from zenrows.batch import Calendar, Daily

  client = ZenRowsBatchClient(api_key="YOUR_ZENROWS_API_KEY")

  job = client.submit_scheduled(
      Calendar(times_of_day=["09:00"], cadence=Daily(), timezone="America/New_York"),
      [{"url": "https://www.scrapingcourse.com/ecommerce/", "external_id": "catalog"}],
      zenrows_params={"mode": "auto", "extract": "auto"},
      name="daily-catalog",
  )
  data = client.get_job(job.job_id).data
  print("Scheduled", job.job_id, "next run:", data.next_scheduled_run)
  ```

  ```javascript Node.js theme={"dark"}
  const response = await fetch("https://async.api.zenrows.com/v1/jobs", {
    method: "POST",
    headers: { "X-API-Key": "YOUR_ZENROWS_API_KEY", "Content-Type": "application/json" },
    body: JSON.stringify({
      type: "scheduled",
      status: "closed",
      name: "daily-catalog",
      zenrows_params: { mode: "auto", extract: "auto" },
      schedule: {
        calendar: { times_of_day: ["09:00"], cadence: { daily: {} } },
        timezone: "America/New_York",
      },
      tasks: [{ url: "https://www.scrapingcourse.com/ecommerce/", external_id: "catalog" }],
    }),
  });
  if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);

  const job = await response.json();
  console.log("Scheduled", job.job_id);
  ```

  ```bash cURL theme={"dark"}
  curl -X POST "https://async.api.zenrows.com/v1/jobs" \
    -H "X-API-Key: YOUR_ZENROWS_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "type": "scheduled",
      "status": "closed",
      "name": "daily-catalog",
      "zenrows_params": { "mode": "auto", "extract": "auto" },
      "schedule": {
        "calendar": { "times_of_day": ["09:00"], "cadence": { "daily": {} } },
        "timezone": "America/New_York"
      },
      "tasks": [{ "url": "https://www.scrapingcourse.com/ecommerce/", "external_id": "catalog" }]
    }'
  ```
</CodeGroup>

`GET /jobs/{job_id}` then shows the stored schedule, the URLs as `template_tasks`, `schedule_state` and `next_scheduled_run` in UTC:

```json theme={"dark"}
{
  "job_id": "01M4YAXZH53S0Y48W1DS3F67FS",
  "type": "scheduled",
  "name": "daily-catalog",
  "schedule": {
    "calendar": { "times_of_day": ["09:00"], "cadence": { "daily": {} } },
    "timezone": "America/New_York"
  },
  "next_scheduled_run": "2026-10-15T13:00:00Z",
  "schedule_state": "active",
  "template_tasks": [
    { "external_id": "catalog", "url": "https://www.scrapingcourse.com/ecommerce/" }
  ]
}
```

## Example output

Creating the job with the Python SDK prints the job ID and the first run time in UTC:

```text theme={"dark"}
Scheduled 01M4YAXZH53S0Y48W1DS3F67FS next run: 2026-10-15 13:00:00+00:00
```

## Pick a schedule

Send one of `rate`, `calendar` or `at`. `calendar` and `at` take an IANA `timezone`, such as `Europe/Berlin`, so runs follow local time through daylight saving changes. Times of day are on the hour, in `HH:00` form.

| You want | `schedule` | Python SDK builder |
| - | - | - |
| Every 6 hours | `{"rate": {"every": 6, "unit": "hour"}}` | `Rate(every=6, unit="hour")` |
| Every day at 09:00 | `{"calendar": {"times_of_day": ["09:00"], "cadence": {"daily": {}}}, "timezone": "America/New_York"}` | `Calendar(["09:00"], Daily(), "America/New_York")` |
| Weekdays at 08:00 and 20:00 | `{"calendar": {"times_of_day": ["08:00", "20:00"], "cadence": {"weekly": {"days": ["mon", "tue", "wed", "thu", "fri"]}}}, "timezone": "America/New_York"}` | `Calendar(["08:00", "20:00"], Weekly(days=[...]), "America/New_York")` |
| The 1st and 15th of each month | `{"calendar": {"times_of_day": ["06:00"], "cadence": {"monthly": {"days": [1, 15]}}}, "timezone": "Europe/London"}` | `Calendar(["06:00"], Monthly(days=[1, 15]), "Europe/London")` |
| Once, at a set time | `{"at": "2026-11-02T09:00:00", "timezone": "Europe/Madrid"}` | `At(datetime(2026, 11, 2, 9, 0), timezone="Europe/Madrid")` |

`rate` runs every 60 minutes or more, in `minute`, `hour` or `day` units. Write `at` without a `Z` or offset: the `timezone` sets the zone.

## Read the latest run's results

Each fire creates a run. `GET /jobs/{job_id}` returns the newest one as `latest_run`, and `GET /jobs/{job_id}/results` lists that run's results:

<CodeGroup>
  ```python Python (Requests) theme={"dark"}
  import requests

  BASE = "https://async.api.zenrows.com/v1"
  headers = {"X-API-Key": "YOUR_ZENROWS_API_KEY"}
  job_id = "YOUR_JOB_ID"

  job = requests.get(f"{BASE}/jobs/{job_id}", headers=headers, timeout=30).json()
  run = job["latest_run"]
  print("Run", run["run_sequence"], run["status"], run["stats"]["successful"], "successful")
  print("Next run at", job["next_scheduled_run"])

  results = requests.get(
      f"{BASE}/jobs/{job_id}/results",
      headers=headers,
      params={"status": "successful"},
      timeout=30,
  ).json()["results"]
  for row in results:
      parsed = requests.get(row["result_url"], timeout=60).json()["parsed"]  # same JSON as extract=auto
      print(row["external_id"], [(p["name"], p["price"]) for p in parsed["products"][:3]])
  ```

  ```bash cURL theme={"dark"}
  curl "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID" -H "X-API-Key: YOUR_ZENROWS_API_KEY"
  curl "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/results?status=successful" -H "X-API-Key: YOUR_ZENROWS_API_KEY"
  ```
</CodeGroup>

After a `rerun`, the script prints:

```text theme={"dark"}
Run 1 completed 1 successful
Next run at 2026-10-15T13:00:00Z
catalog [('Abominable Hoodie', 69), ('Adrienne Trek Jacket', 57), ('Aeon Capri', 48)]
```

`GET /jobs/{job_id}/runs` lists every run, newest first, to compare one run with the next. To hear about each run as it ends, add a [webhook](/academy/batch/webhooks) to the job.

## Change, pause or resume the schedule

Replace the schedule with `PUT /jobs/{job_id}/schedule`, and pause or resume it with `POST /jobs/{job_id}/schedule/state`. Pausing stops new runs from firing; the job, its URLs and past runs stay as they are.

<CodeGroup>
  ```bash cURL theme={"dark"}
  # Switch to 09:00 and 18:00 on Monday, Wednesday and Friday
  curl -X PUT "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/schedule" \
    -H "X-API-Key: YOUR_ZENROWS_API_KEY" -H "Content-Type: application/json" \
    -d '{"calendar": {"times_of_day": ["09:00", "18:00"], "cadence": {"weekly": {"days": ["mon", "wed", "fri"]}}}, "timezone": "America/New_York"}'

  # Pause, then resume
  curl -X POST "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/schedule/state" \
    -H "X-API-Key: YOUR_ZENROWS_API_KEY" -H "Content-Type: application/json" \
    -d '{"schedule_state": "paused"}'
  curl -X POST "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/schedule/state" \
    -H "X-API-Key: YOUR_ZENROWS_API_KEY" -H "Content-Type: application/json" \
    -d '{"schedule_state": "active"}'
  ```

  ```python Python (SDK) theme={"dark"}
  from zenrows import ZenRowsBatchClient
  from zenrows.batch import Rate

  client = ZenRowsBatchClient(api_key="YOUR_ZENROWS_API_KEY")
  job = client.job("YOUR_JOB_ID")

  job.schedule.update(Rate(every=6, unit="hour"))  # replace the schedule
  job.schedule.pause()                             # stop firing new runs
  job.schedule.resume()                            # start again
  print(client.get_job(job.job_id).data.schedule_state)
  ```
</CodeGroup>

A paused job reads `"schedule_state": "paused"` and fires no new runs. Its `next_scheduled_run` keeps showing the slot that fires once you resume.

`job.schedule.*` acts on the schedule. `job.run.pause()` and `job.run.stop()` act on the run in progress, which is a different control.

## Run it now or delete it

Start a run right away, outside the schedule, with `POST /jobs/{job_id}/rerun`. Delete the job, its schedule, runs and results with `DELETE /jobs/{job_id}`:

```bash cURL theme={"dark"}
curl -X POST "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID/rerun" -H "X-API-Key: YOUR_ZENROWS_API_KEY"
curl -X DELETE "https://async.api.zenrows.com/v1/jobs/YOUR_JOB_ID" -H "X-API-Key: YOUR_ZENROWS_API_KEY"
```

## Customize it

| Change | How |
| - | - |
| Many URLs | Add them to `tasks`, or upload a CSV and pass `file_input_id`, see [Large inputs](/batch/developer-guide-restapi#large-inputs-csv-upload) |
| Pages as markdown instead of JSON | `"response_type": "markdown"` in `zenrows_params` |
| Your own fields | `"css_extractor"` in `zenrows_params`, see [Extract data with CSS selectors](/academy/how-to-extract-data) |
| A notification after each run | Add `"webhook": {"url": ..., "signature": true}`, see [Get notified when a Batch job finishes](/academy/batch/webhooks) |

## Troubleshooting

* **`400` with `invalid_argument` on the schedule**: the `detail` names the field. Use `HH:00` times, an IANA timezone, and a `rate` of every 60 minutes or more.
* **A run is `failed` with a `failure_reason`**: scheduled runs check credits and [credit caps](/first-steps/api-key-credit-caps) when they start. Fix the account or key, then let the next run fire or start one with `rerun`.
* **No run fired at the expected time**: check `schedule_state` and `next_scheduled_run` (UTC) on the job. A paused schedule reads `paused`.

## Related recipes

<CardGroup cols={2}>
  <Card title="Get notified when a Batch job finishes" icon="bell" href="/academy/batch/webhooks">
    Receive a signed webhook after every run.
  </Card>

  <Card title="Scrape a list of URLs" icon="list" href="/academy/scrape-from-a-list-of-urls">
    One-time Batch jobs from every surface.
  </Card>

  <Card title="Get structured JSON from any page" icon="wand" href="/academy/extract/auto-extraction">
    What `extract=auto` returns on each run.
  </Card>

  <Card title="Extract real estate listings and property data" icon="home" href="/academy/extract-property-data">
    Re-extract listings on a schedule.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.