Skip to main content
In Lesson 4 you scraped every product once. A tracker earns its name when it runs on its own. In this last lesson you hand the job to Zenrows as a daily schedule, collect each day’s results as a dated snapshot, and learn how to get notified when a run finishes. What you’ll build:
  • schedule_tracker.py creates a scheduled Batch job that scrapes every product at 09:00 each day.
  • run_now.py fires the job immediately so you can test it.
  • save_snapshot.py writes the latest run to products-YYYY-MM-DD.json.
  • stop_tracker.py pauses and deletes the schedule.
  • Optionally, webhook_receiver.py gets a signed run.completed event after every run.
What you’ll learn:
  • How a scheduled job stores its URLs as a template and creates a new run on every fire.
  • How to set a calendar schedule with a timezone.
  • How to verify a signed webhook from Zenrows.
Time: about 10 minutes.

Create the daily schedule

A scheduled Batch job keeps its tasks and zenrows_params as a template. Each time the schedule fires, Zenrows starts a fresh run of those tasks, the same kind of run you waited on in Lesson 4: one Fetch request per URL. You set the cadence with one of three builders from zenrows.batch: Rate (every 60 minutes or more, in minute, hour or day units), Calendar (times of day on a daily, weekly or monthly cadence, in a timezone) or At (one future time). Create schedule_tracker.py. It reuses product_urls.txt from Lesson 3 and PRODUCT_SELECTORS from Lesson 2:
Python (SDK)
Run python schedule_tracker.py:
The next run is 09:00 Madrid time, shown in UTC. The script saves the job ID to tracker_job_id.txt so the next scripts can find the job. Set timezone to any IANA timezone name, and times_of_day to full hours in 24-hour format, such as "09:00" or "18:00".

Test it with a run now

You don’t have to wait until tomorrow. rerun() on a scheduled job that hasn’t fired yet starts a run from its template right away. Create run_now.py:
Python (SDK)
client.job(job_id) points at the job by ID without fetching it first. Run python run_now.py:

Save a dated snapshot

Each run’s results stay available after it finishes. save_snapshot.py reads the job’s latest run and writes it to a file named after today’s date, so every day adds one snapshot you can compare with the day before:
Python (SDK)
Run python save_snapshot.py:
Run save_snapshot.py after each daily run, by hand, from a cron job, or from the webhook in the next section.

Get notified when a run finishes

A webhook tells your app the moment a run ends, so it can save the snapshot without polling. Zenrows POSTs a run.completed event when a run completes and a run.failed event when it fails. With signature: True, each delivery is signed with your organization’s HMAC key, so your receiver can prove it came from Zenrows.
1

Create a signing key and attach the webhook

Your receiver needs a public HTTPS URL. This script creates your organization’s HMAC key, prints its secret (Zenrows returns it once, so store it now) and attaches the webhook to the tracker job:
Python (SDK)
If your organization already has a key, reuse its secret and skip rotate_hmac_key(). See Manage HMAC keys.
2

Verify each delivery

Each signed delivery carries an X-Signature header in the form t=<unix>,v1=<hex>,kid=<key id>. Recompute HMAC-SHA256(secret, "<t>.<raw body>") with the base64-decoded secret and compare it to v1. Deliveries arrive at least once, so skip any X-ZenRows-Event-Id you’ve already handled. This Flask receiver (pip install flask) does both:
Python (Flask)
The receiver verifies the raw request body as received, before any JSON parsing, so the signature always matches. The event body carries event_type, job_id, run_id and the run’s stats, so your handler knows which run to download.
In production, keep seen_event_ids in your database instead of memory, and keep the secret in an environment variable or a secret manager.

Pause or delete the schedule

A scheduled job keeps firing, and spending credits, until you pause or delete it. When you’re done with the course, run stop_tracker.py:
Python (SDK)
To keep the job and stop it for a while, call job.schedule.pause() alone, and job.schedule.resume() later. To change the cadence, use job.schedule.update(...) with a new builder, see Scheduled jobs.

Ask your AI agent

An agent handles both halves of the tracker: a coding agent sets up the schedule, and any MCP client checks the results whenever you ask. The schedule itself runs in the Zenrows cloud, so nothing needs to stay open on your side. Set up the daily schedule. 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.
The same prompt schedules a catalog of thousands of products: every URL in the file becomes part of the daily job. Check the latest run. With the Zenrows MCP server connected to Claude, Cursor or any MCP client, paste this prompt with your job ID. Your agent picks the right Zenrows tools and settings on its own.

What you built

Your tracker now runs itself. Over six lessons you used all five Zenrows primitives in one project:

Where to go next

Turn a whole catalog into a structured dataset

The production version of this course: crawl, scrape and store a full catalog.

Detect changes on any page and get alerted

Compare each day’s snapshot with the last one and alert on what changed.

Choose the right Zenrows primitive

When to reach for Fetch, Extract, Crawl, Batch or Browser on your own project.

Zenrows Academy

Browse every recipe by goal, surface and industry.