Skip to main content

Auto-select loses jobs because it looks once, at the wrong moment

This page explains why jobs with applicants still expire with nobody selected at outlets where auto-select is switched on. It is the second page of the auto-select timing initiative. The numbers come from the nightly copy of production, jodgig_ai, for shifts starting 22 June to 13 September 2026, twelve full weeks. Two analysts measured them, one blind to the other, and got the same counts.

Terms used on this page:

termmeaning
a missa jod_jobs row that expired with applicants and nobody selected (status: 8), at an outlet with locations.is_auto_select_applicant = 1, where at least one applicant was still live and eligible at expiry. 703 jobs in the twelve weeks.
the run for a jobthe one scheduled auto-select attempt for that job: 09:00 the day before the shift if it starts before noon, 17:00 the day before if it starts at noon or later.
normal jobposted 24 hours or more before the shift. It expires 8 hours before the shift.
short-notice jobposted less than 24 hours before the shift. It expires at the shift start.
reposta jod_jobs row with prev_job_id set. The system creates one when a selected jodder drops out, with created_at equal to that moment and the original start time.

1. Three things happen to a missed job, and the run is to blame for only one of them​

Every one of the 703 misses falls into exactly one bucket (fact from data).

what happenedmissesshareof which short-noticeof which repostsof which UKG jobs
the job was created after its run had already passed51573%504232244
the job existed at the run, but the first live applicant arrived after it14721%82427
the job and a live applicant were both there, and the run did nothing416%21831

So 662 of the 703 (94%) were never in front of the run. The job or the applicant arrived after the one moment it looked. The selection code itself failed on 41 jobs, and 31 of those are UKG jobs, where a hire needs a live call to UKG that can fail five ways and rolls back without a log. Most of the other ten had a top applicant with no rank yet, which the code sorts first and then gives up on. From 23 June on, this bucket holds 17 jobs in twelve weeks, about 1.5 a week.

The jobs in the first bucket were created a median 10.5 hours before the shift. 247 of the 515 were ready, meaning open with a live applicant, less than 6 hours before the shift. No schedule that keeps 6 hours of notice for the jodder can catch those. Four more such jobs sit in the second bucket, so 251 of the 703 misses (36%) are out of reach of any schedule at a 6-hour floor.

2. The two run times sit before the applicants arrive and after the jobs expire​

Four facts explain the first two buckets (all fact from data, same twelve weeks).

factnumber
applications made between 17:00 and 08:59, after the evening run and before the morning run43,304 of 78,252 (55%)
applications made between 17:00 and 23:59 alone29,837 of 78,252 (38%)
jobs posted between 17:00 and 08:5912,774 of 25,630 (50%)
jobs posted in the 17:00 hour, minutes after the evening run3,398 of 25,630 (13%)
misses whose shift starts between 07:00 and 18:59710 of 778 (91%)

Put those on one clock. A normal job for a shift before 17:00 expires between 23:00 the night before and 08:59. The 09:00 run finds it already expired. Its only run was 17:00 the day before, which came before 55% of the applications. A job posted in the evening for tomorrow morning is never seen by any run: the 17:00 run has passed, and the 09:00 run looks at the day after tomorrow. The two constants collide: run times set for office hours, and an expiry set 8 hours before the shift.

2.1 Applicants arrive after the run, and the job expires before the next one​

Legend: a milestone is one moment. Red is a period in which the job is open, a live applicant is waiting, and no automatic attempt will happen. Job A is the second bucket in section 1. Jobs B and C are the first bucket. Job C is what the repost cron produces: the clone is created at the moment of the dropout, so it is a short-notice job by construction and gets no run.

2.2 The hiring manager's own decision window sits next to the run, not around it​

Managers select a median 15.5 hours after the first applicant on a normal job, and a median 0.5 hours on a short-notice job. 71% of their selections happen between 06:00 and 17:59, while 42% of applications arrive outside those hours. The 09:00 and 17:00 reminder emails barely move them. The three jobs below show the three outcomes that produce the counts on the current metrics page.

Legend: blue is the manager's decision time, measured from the first applicant. Job D is the usual case, and it is why the day-before run takes only 0.6% of manager picks today. Job E is that 0.6%. Job F is a miss: the applicant came after the run, and the manager did not act before expiry. The solution page has to catch Job F without turning more Job D cases into Job E cases.

3. Reposting after a dropout manufactures short-notice jobs with no applicants​

When a selected jodder cancels or is removed, the system does not re-open the job. It clones it into a new jod_jobs row with created_at equal to the moment of the clone, the original start time, and no applicants. The parent job's other applicants stay behind as unselected.

fact, twelve weeksnumber
reposts3,126 (about 260 a week)
reposts created under 24 hours before the shift, so short-notice by construction1,124 of 3,126 (36%)
reposts that expired with applicants and nobody selected396 of 3,126 (13%)
reposts that expired with no applicant at all571 of 3,126 (18%)
reposts completed1,302 of 3,126 (42%), worth SGD 137,253 of credits
parent jobs that still had other applicants when the clone was made1,496 of 3,126 (48%)
those applicants who found the clone and applied again by themselves2,051 of 3,688 (56%)
reposts that ended up selecting one of the parent's applicants142 of 3,126 (5%), completed at 86%

The clone is not the reason the credits are lost. Its applicants mostly come back. The reason is that nobody selects them: the clone gets no run, and the manager who already chose once often does not choose again.

4. Four defects in the code turn a caught job into a lost one​

These are facts from the code at merge-prod, verified against the data.

defectwhat the code does, and wherewhat it cost in twelve weeks
failures are thrown awayJobAutoSelectApplicantCommand::handle assigns the result of processBatch to a variable and never reads it. No log, no counter. File: app/Console/Commands/JobAutoSelectApplicantCommand.php.The catch of any change cannot be measured until a log line exists.
UKG hires roll back silentlyA UKG job needs a live UKG call. It fails when the shift is not found, is unassigned, belongs to an employee outside the allow list, already carries another platform's comment, or has started. Any failure rolls the selection back with no retry. Files: app/Services/UKGService.php:190-252, app/Services/AutoSelectApplicantService.php:47-107.31 of the 41 jobs where the run had its chance and did nothing.
a missing rank wins, then loses the jobThe pick orders by applicant_rank ascending, and MySQL sorts NULL first. An applicant whose rank was never computed, or was cleared by a withdrawal, is picked first. If that pick fails, the run does not try the next applicant. File: app/Services/AutoSelectApplicantService.php:238.23 of the 41 had a top applicant with no rank.
the clone carries the parent's auto-select markerThe repost command copies the whole row and resets only hired_by_manager_id, no_users_apply_job, created_at and prev_job_id. auto_select_user_id stays. File: app/Console/Commands/JodJobAutoJobPostingCommand.php:137-167.207 clones showed as auto-selected with no selection on them. 169 were never selected at all. Every report on reposts overstates auto picks unless it compares the marker with the parent's.

5. Five rules in the code bound any fix​

rulevaluewhy it matters for the fix
ACK1 closes 3 hours before the shifta jodder may acknowledge only between the hire and job_start_date minus 3 hoursany preparation floor under 3 hours leaves the jodder unable to acknowledge. 6 hours is safe, 4 is the tightest sensible setting.
expiry is its own clock8 hours before the shift for a normal job, at the shift for a short-notice one, checked every 10 minutes, 100 jobs a runa run placed after a job's expiry finds nothing. The expiry command already visits every job at that moment.
nothing runs from 22:00 to 23:59expiry, no-show, reposting, completion and every reminder pause; auto-select does nota hire at 22:30 happens while the reminders and the expiry are asleep
the six selection commands run one after anotherKernel.php uses no background execution, so pre-selection, the day-before run and the short-notice run execute in order at 09:00 and at 17:00there is no race between the runs today. A design that selects concurrently must add a row lock, because processAutoSelect only re-reads the applicant row.
the day-before run has no creation-time filter, by decisionit takes every open job in its window, whatever the posting time. The CTO removed the filter in the review of PR 2719 because it only delayed selections.a short-notice job that exists at the day-before run is selected there, with more notice, and the short-notice run then skips it