Runs every morning at 06:00. Searches 18 job titles across Germany, drops obvious misses with cheap rules, has GPT-4.1 score what survives against a written rubric, and writes the result to a Notion board before I wake up.
How it works
I built this because the expensive part of a job search is reading. Most postings are not a fit and a person can tell in ten seconds, but it takes ten seconds each, every day, across dozens of titles.
Search Config emits one item per title. Each becomes a call to a LinkedIn jobs actor on Apify, scoped to Germany with a seven-day window. Normalise Jobs keeps only the target cities plus remote roles, and logs how many it dropped so the filtering is visible. Two traps it handles deliberately: Frankfurt must not match Frankfurt am Main, and Brandenburg must not match the state.
Find New Jobs dedupes twice, by URL and by company plus role, because one posting surfaces under two title queries with two URLs. Knockouts then rejects on rules alone, before any model call, so an obvious miss never costs a token. Knocked-out rows are still written to Notion. If they were not, dedupe would never see them and they would come back tomorrow.
Rank Job reads up to 12,000 characters of the description and returns a structured verdict: score, reason, knockout, German requirement and function. Decide Status merges that with the record and picks a Notion status. The floor is 60, because the rubric itself says below 60 do not apply. Contradictions resolve toward rejection: a stated German requirement is a knockout regardless of which field carries it.
Key features
- Cheapest checks first: city filter, dedupe and rule knockouts run before any model call
- Structured verdicts from GPT-4.1 with a written rubric and a 60-point floor
- The full posting is stored on the Notion page in blocks, so a row can be re-judged later without re-scraping
- When a posting names a recruiter, a contact row is created and linked to the application
- A failed ranking is swept into a separate rescore workflow rather than lost
- Auth errors stop the run loudly instead of reporting success with nothing written
Step by step
- Daily DiscoverySchedule trigger at 06:00, before the outreach workflow.
- Search ConfigOne item per title, 18 titles.
- Apify LinkedIn JobsOne HTTP call per title, synchronous, 25 results each, seven-day window.
- Normalise JobsFlatten, map seniority, keep target cities and remote, log the drops.
- Get Existing Applications and Find New JobsRead the board, dedupe by URL and by company plus role.
- KnockoutsRule-based rejections. Nothing here costs a model call.
- Rank JobGPT-4.1 scores the survivors with a structured verdict.
- Decide Status and Create Application RowPick the status, write the row, store the posting in blocks.
- Contacts and sweepCreate a linked contact when a recruiter is named; hand unparsed verdicts to the rescore workflow.
The run that scored everything zero
On 5 September the run wrote 61 jobs as not a fit with a match of zero. Decide Status read the model's answer at one path when the API actually nested it somewhere else. Every verdict failed to parse, every score defaulted to zero, and the run reported success.
Because the postings had been stored on each Notion page, the rows could be re-judged without re-scraping. That is the whole reason the storage exists. Decide Status now walks four candidate response shapes and counts the failures, so the same bug announces itself next time.