A Cloudflare Worker cron job that searches Google (via the Serper.dev API) for jobs posted on ATS (Applicant Tracking System) sites and sends new results to a webhook. Runs once every 24 hours. (you can change to run hourly if you want)
Cost of running
Serper.dev has a generous 2,500 free credit. If you search for 5 queries in one country, each run will cost: 5 queries x 19 Ats = 95 credit.
If you run the cron once a day, that's enough for almost a month daily alert for free.
CloudFlare cost is FREE too.
How it works
- On its cron schedule the worker reads
config.json. - It builds the cartesian product of countries × queries × date_range, and runs each one against every ATS in
ats.json— one Serper search per combination, e.g.site:myworkdayjobs.com "Outside IR35"withgl=gbandtbs=qdr:d. - Organic results are collected, de-duplicated by URL, and checked against a Cloudflare KV store so jobs seen in a previous run are skipped.
- New jobs are POSTed to your webhook (in batches of 100) and then marked as seen in KV.
Configuration — config.json
Each key is an array of strings that you can customise for your search:
| Key | Meaning | Example |
|---|---|---|
countries |
Google country codes (gl) |
["gb", "us"] |
queries |
Search terms (see quoting below) | ["\"Outside IR35\"", "Senior Java Engineer"] |
date_range |
Time filters (see below) | ["past_day"] |
Every search runs against all ATS sites listed in ats.json (a friendly name → hostname map). Add or remove providers by editing ats.json and redeploying, no code change needed.
Duplicate hostnames are de-duplicated automatically. The list currently includes Ashby, Workable, Workday, Greenhouse, Lever, SmartRecruiters, and more. See ats.json for the full set.
Search terms (quoting)
Each queries entry is placed verbatim after the site: filter, so you control exact-phrase vs. loose matching with quotes inside the JSON string:
config.json value |
Resulting query |
|---|---|
"\"Registered Nurse in New York\"" |
site:{ats} "Registered Nurse in New York" |
"Registered Nurse in New York" |
site:{ats} Registered Nurse in New York |
date_range values map to Google's qdr: time filter:
| Value | tbs |
|---|---|
anytime |
(none) |
past_hour |
qdr:h |
past_day |
qdr:d |
past_week |
qdr:w |
past_month |
qdr:m |
past_year |
qdr:y |
Editing config.json requires a redeploy (npm run deploy), it is bundled into the worker.
Webhook payload
{
"source": "directjobs-cron",
"timestamp": "2026-06-28T00:00:00.000Z",
"count": 1,
"jobs": [
{
"title": "DevOps, AI and Automation Engineer (Contract)",
"url": "https://tmhcc.wd108.myworkdayjobs.com/...",
"snippet": "Engagement Type: Outside IR35 Contract...",
"date": "5 days ago",
"query": "Outside IR35",
"ats": "Workday",
"country": "gb"
}
]
}How to get the alerts to your Telegram, Email, Slack?
You can use Make or Zapier to create automation, receiving the webhook, then send to whatever platform you like.
Setup & deploy
# 1. Install deps npm install # 2. Log in to Cloudflare npx wrangler login # 3. Create the KV namespace and paste the printed id into wrangler.toml npx wrangler kv namespace create SEEN_JOBS # 4. Add your secrets npx wrangler secret put SERPER_API_KEY npx wrangler secret put WEBHOOK_URL # destination for webhook mode (the default) # 5. Deploy npm run deploy
Local development
cp .dev.vars.example .dev.vars # fill in your key + webhook npm run dev # then hit http://localhost:8787/run
GET /runtriggers a search run manually and returns a JSON summary.- To test the cron path locally:
curl "http://localhost:8787/cdn-cgi/handler/scheduled".
Export a CSV locally
To get the current results as a CSV file on disk — no dev server, no webhook needed:
npm run export:csv # writes ./jobs.csv npm run export:csv out.csv # writes ./out.csv
It runs the same Serper search as the worker and writes the URL-deduplicated results straight to the file, one row per job (title, url, snippet, date, query, ats, country). It reads SERPER_API_KEY from the environment or .dev.vars. (This path doesn't use the KV "seen" store, so it always writes the full current result set.)
Changing the schedule
Edit the cron expression in wrangler.toml (triggers.crons) and redeploy.
"0 0 * * *" = daily at 00:00 UTC;
"0 * * * *" = hourly.
