Scheduling & the tick
The background jobs, what one cycle does, and how work is budgeted.
The pipeline runs itself. Six macOS LaunchAgents start small, bounded slices of work on a timer; each takes a lock, spends a time budget, records what it did, and exits. Nothing runs forever, so nothing hangs forever.
tick.scrapers_frozen: false, scheduler.daily_scrape: true).Referrals are Apify's job: the Apify job buys LinkedIn People tabs for the shortlist, tech team only, and the free people-search job stays off (tick.free_referral_job: false). The shortlist job completes one shortlisted company every 10 minutes. To pause all scraping, set tick.scrapers_frozen: true and run tick install.The background jobs#
| Agent | Command | Schedule | Lock |
|---|---|---|---|
| com.zonuly.tick.free | tick run --kind free | every 10 min, and at login | data/tick.lock |
| com.zonuly.tick.paid | tick run --kind paid | 06:15 daily | data/tick.lock |
| com.zonuly.tick.send | tick run --kind send | every 5 min | data/send.lock |
| com.zonuly.audit | audit run | hourly | — |
| com.zonuly.apify | tick run --kind apify | every 30 min, and at login | data/apify.lock |
| com.zonuly.shortlist | tick run --kind shortlist | every 10 min, and at login | data/shortlist.lock |
| com.zonuly.referrals | referrals run | off (tick.free_referral_job: false) | data/referrals.lock |
A job that finds its lock held exits at once with {"skipped": "another tick is running"}. Sending has its own lock so it never waits behind a 40-minute free tick. Logs go to logs/tick-*.log, logs/apify.log, logs/shortlist.log and logs/audit-run.log.
Inside one free tick#
The free tick has a 40-minute budget (tick.budget_seconds: 2400), split 70 / 30 between finding people and working on companies. Every step runs inside its own error boundary — one failing stage never ends the tick.
- 1Read replies from Gmail; draft follow-ups into review
- 2Full board scrape — once per day
- 3Hygiene: merge duplicates, quarantine junk
- 4Guard: expire stale hiring proof, recompute completeness
- 5Refresh priority scores
- 6People: find referrers at the next companies in the queue
- 7Addresses: learn patterns, address ready referrers (MX-checked)
- 8Hunter (2 lookups) and one Apify People tab, inside their budgets
- 9Re-verify referrers whose evidence is 90+ days old
- 10Fill: complete up to 20 companies (★ list → India → remote)
- 11Rotating company steps: funding RSS, websites, boards, careers, revalidate, verify, YC, India discovery
- 12Cheap tail: pay extraction, grading, pay floor, selection odds, the decision brain, the complete bar
Rotation and saturation#
Company steps take turns through a saved cursor, so each gets its share over successive ticks. When the fill steps find almost nothing left to do, the tick spends the spare time discovering new companies instead.
The paid tick#
Once a day at 06:15 (and in hourly slices) the paid tick spends the metered budgets — Exa, scrape.do and model calls — paced through the day: by hour h, at most cap × (h+1) / 24 of a daily cap may be used. It discovers companies, fills facts, writes descriptions and stories, reads levels.fyi, regrades everything, and rebuilds the knowledge-graph brief.
The free referral job (off)#
Off since 23 Oct 2026: referrals come from Apify. When tick.free_referral_job is true, every 15 minutes a 20-minute cycle works only on people, city by city: Bengaluru (★ list first), then Hyderabad. It claimsthe companies it is working on, so the free tick's people step never searches the same company at the same time. Per company: YC founders and team pages → one Exa people search → public LinkedIn (city-scoped) → GitHub → public mentions → web addresses → address fill.
The Apify job#
The referral engine. Every 30 minutes, it buys LinkedIn People tabs from Apify for up to three companies — the decision brain's shortlist first (Bengaluru by rank, then Hyderabad, then Remote), any company with fewer than two engineers on file — searching only engineering titles (actors.apify.tech_titles), since Apify bills per profile — 25 people per company, in the company's own city, each company bought once, inside the day's Apify cap (actors.apify.daily_usd: 0.60, the $19 Starter credit spread over a month), never overlapping itself. Each run first reads up to 30 homepages, for free, for the company's LinkedIn page, because a People tab can only be bought for a company whose page is known. When the day's money is spent, it stops until midnight. The mixer draws on the same cap, and always leaves $0.30 of it for this job.
The send job#
Every 5 minutes, it sends at most oneapproved email whose time slot has come. The day's 25 slots are spread across 10:00–19:00 with jitter, the same for every run that day. Details in Sending & deliverability.
The API's own scheduler#
When the API is running (scripts/run.py serve), APScheduler adds two jobs: the daily cycleat 08:00 (scrape if the tick hasn't, salaries, lexical prefilter, rubric scoring, notifications, graph sync) — off while scheduler.daily_scrape is false — and a reply cycle every hour. Rubric scoring with the model runs only here, not in the tick. The mixer is never scheduled anywhere: it spends only when asked.
python scripts/run.py tick status # loaded? last run? what it did
launchctl list | grep zonuly # what macOS has loaded
launchctl unload ~/Library/LaunchAgents/com.zonuly.tick.send.plist # pause sending
launchctl load ~/Library/LaunchAgents/com.zonuly.tick.send.plist # resumecaffeinate running. Keep it open and on power during the send window, or sends wait until it wakes.