Post a Daily Feed of New Tech Jobs to Slack or Telegram
Send each morning's new Hirist.tech jobs for your stack and cities to a Slack or Telegram channel, with repeats filtered out by job id, using Python.
The actor referenced in this article. Pay only for results delivered.
A job alert channel lives or dies on two things: it posts every morning, and it never posts the same job twice. Community admins running a Telegram group for Bangalore developers, bootcamps with a Slack for their alumni, and newsletter writers who list fresh roles all hit the same wall. Hirist.tech has the jobs, but checking it by hand each day and remembering what you already shared does not last a month.
Try it live: Hirist Jobs Scraper: 147 Locations, 19 Fields, No Login. Pay per result delivered. Failed and empty results are never charged.
The Hirist Jobs Scraper pulls the last day or two of Hirist jobs for a category or keyword and set of cities. A short Python script then drops anything already posted and sends the rest to your channel.
What you get and who it is for
Each job arrives with the fields a good alert needs:
| Field | Used in the post for |
|---|---|
job_id | The key that stops a job being posted twice |
title, company | The headline |
locations, remote | Where, and whether it is work from home |
experience_min_years, experience_max_years | The seniority band, such as 4 to 7 years |
salary_min_lpa, salary_max_lpa | Pay in lakhs per annum, when the recruiter shows it |
company_rating | AmbitionBox rating, when Hirist shows it |
apply_count | How crowded the job already is |
posted_at | Posting time in UTC, shown in IST in the post |
url | The apply link on hirist.tech |
This is for anyone who owns a channel of engineers: Telegram and WhatsApp community admins, Slack groups for alumni or meetups, newsletter writers, and placement teams who want one place to point people every morning.
The input
{
"categorySlug": "backend-development-jobs",
"locations": ["Bangalore", "Hyderabad", "Pune"],
"postedWithinDays": 2,
"maxJobs": 200
}
categorySlugpicks one of Hirist’s 12 tech categories, frombackend-development-jobsandfull-stack-jobstodevops-sre-jobs,ai-ml-jobsandcybersecurity-jobs. For a narrower channel, send akeywordsuch asgolangorreact nativeinstead of, or as well as, the category.locationstakes plain city names; several mean “any of these”. AddRemotefor work from home roles, orDubaiorRiyadhfor Gulf openings.postedWithinDays: 2is the important one for a daily feed. The actor’s own test found that a 3 day window returned only jobs from the previous two days, so 2 covers the last day with a margin for jobs posted late in the evening.maxJobsis a ceiling, not a target. Set it above what a normal day returns so a busy Monday is not cut off. Up to 1,000 is allowed.- Add
experienceMinYearsandexperienceMaxYearsif your channel is for a seniority band. The filter keeps jobs whose range overlaps yours.
Why the date filter matters: on September 30 and October 2, 2026 we ran the same input, java developer in Bengaluru with 20 jobs and no date filter. The two runs shared 19 of 20 jobs. Our two browses of the backend category with 30 jobs and no filters shared 27 of 30, and every one of those 30 rows was a featured (premium: true) listing. Without postedWithinDays, a daily run mostly repeats yesterday, and you pay for the repeats.
What the output looks like
These rows are from another run, our browse of backend-development-jobs with 30 jobs and no location or date filter on October 2, 2026. They are two of the jobs posted on September 30, two days before that run, trimmed:
[
{
"job_id": "1676086",
"title": "Vedantu Innovations, Product Engineer, Java",
"company": "Vedantu Innovations",
"company_rating": 3.2,
"company_reviews_count": 1818,
"locations": ["Bangalore"],
"remote": false,
"skills": ["Java", "Spring", "Spring Boot", "SQL", "NoSQL", "System Design", "Backend Architecture"],
"experience_min_years": 2,
"experience_max_years": 5,
"apply_count": 517,
"premium": true,
"posted_at": "2026-09-30T11:18:07.337Z",
"url": "https://www.hirist.tech/j/vedantu-innovations-product-engineer-java-1676086"
},
{
"job_id": "1675913",
"title": "Zaggle, Senior Software Development Engineer, Java",
"company": "Zaggle Prepaid Ocean Services",
"company_rating": 3,
"company_reviews_count": 151,
"locations": ["Hyderabad"],
"remote": false,
"skills": ["Backend Architecture", "Java", "Spring Boot", "Microservices Architecture", "Kafka", "System Design"],
"experience_min_years": 4,
"experience_max_years": 7,
"apply_count": 715,
"posted_at": "2026-09-30T06:40:11.382Z",
"url": "https://www.hirist.tech/j/zaggle-senior-software-development-engineer-java-4-7-yrs-1675913"
}
]
The script below turns each job into a short post like this:
Vedantu Innovations: Product Engineer, Java
Bangalore | 2 to 5 yrs | rated 3.2 | 517 applied | posted 30 Sep, 16:48 IST
https://www.hirist.tech/j/vedantu-innovations-product-engineer-java-1676086
Pay is missing from both rows, which is normal on Hirist. In the actor’s tests it showed on 6 of 200 keyword rows and 82 of 1,000 category rows. When it is there, the script adds it to the post. Every successful run also ends with an info row and every empty run with a no_results row; neither has a job_id, neither is charged, and the script skips both.
Run it in Apify Console
Before you automate it, check the feed looks right:
- Open https://apify.com/themineworks/hirist-jobs-scraper and click Try for free.
- Clear the Keyword field and pick your category, or enter a keyword.
- In Locations, enter your cities one per line.
- Set Posted within to 2 and Max jobs to 200.
- Click Start and look at the Output tab. If a day’s volume is far below what you want, add a city or widen to a second category in another run.
- Click Save as a new task, so the same input can be scheduled or called from code.
Run it from Python
The script runs the actor, keeps only jobs it has never posted, formats them, and sends them to Slack, Telegram or both. It stores the posted job ids in a named Apify key-value store, so it works the same from a laptop, a server or a CI job where local files do not persist.
pip install apify-client requests
import os
import re
from datetime import datetime, timedelta, timezone
import requests
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL")
TG_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
TG_CHAT = os.environ.get("TELEGRAM_CHAT_ID")
IST = timezone(timedelta(hours=5, minutes=30))
run = client.actor("themineworks/hirist-jobs-scraper").call(run_input={
"categorySlug": "backend-development-jobs",
"locations": ["Bangalore", "Hyderabad", "Pune"],
"postedWithinDays": 2,
"maxJobs": 200,
})
jobs = [i for i in client.dataset(run["defaultDatasetId"]).iterate_items() if "job_id" in i]
store = client.key_value_stores().get_or_create(name="hirist-feed-seen")
kv = client.key_value_store(store["id"])
record = kv.get_record("seen")
seen = record["value"] if record else {}
today = datetime.now(IST).date().isoformat()
new_jobs = [j for j in jobs if j["job_id"] not in seen]
new_jobs.sort(key=lambda j: j["posted_at"], reverse=True)
def headline(job):
company = job["company"].strip()
parts = re.split(r"\s+-\s+", job["title"])
if company.lower() == "hirist.tech" and len(parts) > 1:
company, parts = parts[0], parts[1:]
elif len(parts) > 1:
first, name = parts[0].lower(), company.lower()
if first.split()[0] in name or name.split()[0] in first:
parts = parts[1:] # title starts with the company name
return f"{company}: {', '.join(parts)}"
def format_job(job):
posted = datetime.fromisoformat(job["posted_at"].replace("Z", "+00:00")).astimezone(IST)
bits = [", ".join(job["locations"][:3])]
if job["remote"]:
bits.append("remote")
bits.append(f"{job['experience_min_years']} to {job['experience_max_years']} yrs")
if "salary_min_lpa" in job:
bits.append(f"{job['salary_min_lpa']} to {job.get('salary_max_lpa', '?')} LPA")
if "company_rating" in job:
bits.append(f"rated {job['company_rating']}")
if job.get("apply_count") is not None:
bits.append(f"{job['apply_count']} applied")
bits.append(posted.strftime("posted %d %b, %H:%M IST"))
return f"{headline(job)}\n{' | '.join(bits)}\n{job['url']}"
def send(text):
if SLACK_WEBHOOK:
requests.post(SLACK_WEBHOOK, json={"text": text}, timeout=30).raise_for_status()
if TG_TOKEN and TG_CHAT:
requests.post(
f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
json={"chat_id": TG_CHAT, "text": text, "disable_web_page_preview": True},
timeout=30,
).raise_for_status()
for start in range(0, len(new_jobs), 10):
batch = new_jobs[start:start + 10]
header = f"New backend jobs, {today} ({start + 1} to {start + len(batch)} of {len(new_jobs)})"
send(header + "\n\n" + "\n\n".join(format_job(j) for j in batch))
for j in new_jobs:
seen[j["job_id"]] = today
cutoff = (datetime.now(IST).date() - timedelta(days=30)).isoformat()
seen = {k: v for k, v in seen.items() if v >= cutoff}
kv.set_record("seen", seen)
print(f"{len(jobs)} jobs fetched, {len(new_jobs)} posted, {len(jobs) - len(new_jobs)} already seen")
A few choices worth knowing. Posts go out ten jobs per message, which stays well under Telegram’s 4,096 character limit per message. The headline function handles Hirist’s habit of starting the title with the company name (the Zaggle row above posts as Zaggle Prepaid Ocean Services: Senior Software Development Engineer, Java) and listings Hirist posts under its own name, where the real employer is the first part of the title. Job ids are kept for 30 days and then dropped, so the store stays small; with a 2 day window, a job older than that will not come back. Newest jobs are posted first.
To run several channels (backend, AI and ML, DevOps), copy the run input per channel and use a different key-value store name for each.
Schedule it every morning
The simplest setup is a cron entry on any machine that is on in the morning: 30 2 * * * runs at 02:30 UTC, which is 08:00 IST. Set APIFY_TOKEN, SLACK_WEBHOOK_URL or the two Telegram variables in the environment. A scheduled GitHub Actions workflow works the same way, since the seen list lives in Apify and not on the runner.
If you would rather keep the scraping inside Apify, schedule the saved task in Apify Console under Schedules (cron 0 8 * * *, timezone Asia/Kolkata), and change the script to read the latest finished run instead of starting one: client.task("YOUR_TASK_ID").last_run(status="SUCCEEDED").dataset().iterate_items(). Then run the script a few minutes after the schedule, or call it from an Apify webhook on run success.
What it costs
Billing is per job delivered: $4.20 per 1,000 jobs on the Bronze plan, $3.55 on Silver, and $3.00 on Gold and above, plus a flat $0.005 per run. Duplicates inside one run, note rows and empty runs are not charged beyond the start fee.
The daily input has a ceiling of 200 jobs, or $0.845 a day on Bronze. Real days are usually well under the ceiling, so price it on your volume. As an example, a feed that averages 60 jobs a run costs $0.257 a day on Bronze, about $7.71 for 30 days; $6.54 on Silver; and $5.55 on Gold. Some of those 60 will be jobs the previous run already delivered, because a 2 day window run daily overlaps by about a day. You pay for those again; the script just does not post them. To keep a lid on a busy day, set a maximum cost per run on the task, and the actor stops saving jobs once it reaches it.
Limits worth knowing
- No only-new mode. Repeats are removed by your script, and you pay for jobs the previous run already returned. The date filter keeps that overlap small.
- Short windows can come back empty. Our runs with a 1 to 3 day window and an entry-level experience band returned no jobs on several days. Broad categories in big cities fill up; narrow ones may need a weekly digest instead. A second source helps as well, since the same roles come straight off company Greenhouse, Lever and Ashby boards.
- Featured listings dominate without filters. All 30 rows of our unfiltered category browses were premium listings.
- The company field can be a hiring firm or Hirist itself. The headline function handles Hirist’s own listings, but staffing firms still post under their own names.
- Pay is usually hidden. Most posts will have no salary line.
- One run returns at most 1,000 jobs. If a single day ever reaches your
maxJobs, raise it or split the cities across runs.
Related
Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.
Frequently asked questions
Does the actor only return jobs I have not seen? +
No. It has no only-new mode. The script in this guide keeps the job ids it has already posted in an Apify key-value store and skips them, so each job reaches the channel once.
Why use postedWithinDays if the script removes repeats anyway? +
Because repeats still cost money. Two of our runs with the same input two days apart, with no date filter, shared 19 of 20 jobs. With a 2 day window, a run returns only recent posts, so you pay for far fewer jobs you have already seen.
Can I post to WhatsApp or Discord instead? +
Yes, as long as you have a webhook or bot API for it. Replace the send function in the script; the rest stays the same.
What happens on a day with no new jobs? +
The run returns a free no_results row and pays only the $0.005 start fee, and the script posts nothing.
Find Entry-Level AI and ML Jobs in Bangalore Posted This Week
Pull fresh AI and ML jobs for 0 to 2 years of experience from Hirist.tech, merge several searches and rank them by seniority and age with Python.
Map Which Companies Are Hiring for a Tech Stack Across Indian Cities
Turn Hirist.tech job listings into a table of employers by city for one tech stack, with job counts, ratings and top skills, using Python.
Track New Foundit Job Postings Every Day With Monitor Mode
Get only the Foundit.in (Monster India) jobs you have not seen before, every morning, and pay only for the new rows. Input, Python code and real costs.
Track New Shine.com Jobs for a Role Every Day
Get only the Shine.com jobs posted since your last check, for one role and city, using freshness sort, monitor mode and a daily schedule.