Skip to content
Architecture

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.

Who does what, since 23 Oct 2026
Company and role scraping runs on its own again (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#

AgentCommandScheduleLock
com.zonuly.tick.freetick run --kind freeevery 10 min, and at logindata/tick.lock
com.zonuly.tick.paidtick run --kind paid06:15 dailydata/tick.lock
com.zonuly.tick.sendtick run --kind sendevery 5 mindata/send.lock
com.zonuly.auditaudit runhourly—
com.zonuly.apifytick run --kind apifyevery 30 min, and at logindata/apify.lock
com.zonuly.shortlisttick run --kind shortlistevery 10 min, and at logindata/shortlist.lock
com.zonuly.referralsreferrals runoff (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.

  1. 1Read replies from Gmail; draft follow-ups into review
  2. 2Full board scrape — once per day
  3. 3Hygiene: merge duplicates, quarantine junk
  4. 4Guard: expire stale hiring proof, recompute completeness
  5. 5Refresh priority scores
  6. 6People: find referrers at the next companies in the queue
  7. 7Addresses: learn patterns, address ready referrers (MX-checked)
  8. 8Hunter (2 lookups) and one Apify People tab, inside their budgets
  9. 9Re-verify referrers whose evidence is 90+ days old
  10. 10Fill: complete up to 20 companies (★ list → India → remote)
  11. 11Rotating company steps: funding RSS, websites, boards, careers, revalidate, verify, YC, India discovery
  12. 12Cheap tail: pay extraction, grading, pay floor, selection odds, the decision brain, the complete bar
A free tick, in order. Bold steps are the referral work that gets most of the time.

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.

inspect and control
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   # resume
The laptop must be awake
launchd can't run jobs while the Mac sleeps, and closing the lid sleeps it even with caffeinate running. Keep it open and on power during the send window, or sends wait until it wakes.