# Job hunt routine

A scheduled agent routine for aside. Paste the block below in as a new routine,
fill in the placeholders, and it runs on a schedule with browser and tool
access. There are no nodes, no credentials to wire and no JSON. The prompt is
the program: if you want different behaviour, you edit the prose.

Two conventions before you start. Every value written in double braces is yours
to replace, and the defaults are examples rather than advice. And where the
routine says "me", that means you, the person running it.

---

## Placeholders

| Placeholder | Default | What it controls |
| --- | --- | --- |
| `{{ROLE_KEYWORDS}}` | `backend engineer, platform engineer, API developer` | The titles and skills a posting has to be about. |
| `{{EXCLUDE_KEYWORDS}}` | `internship, working student, unpaid, commission only, agency recruiter` | Instant rejects, whatever else the posting says. |
| `{{LOCATIONS}}` | `Remote (Europe), Dublin, Lisbon` | Where you would actually take the job. |
| `{{MAX_POSTING_AGE_DAYS}}` | `7` | How old a posting may be, measured from its verified date. |
| `{{APPLICATIONS_PER_SITE_PER_DAY}}` | `4` | Per site, per calendar day. A ceiling, never a target. |
| `{{NOTIFY_CHANNEL}}` | `a single message in this chat at the end of each run` | Where the run report goes. |

---

## The routine

### Goal

Four times a day, sweep the job sources I have listed, judge each new posting
against the filter hierarchy below, apply to the ones that survive, and report
what happened. Quality of match comes before volume. A run that applies to
nothing and explains clearly why nothing qualified is a good run, not a failed
one.

### Cadence

Run four times a day, at 07:40, 11:40, 15:40 and 19:40 local time.

The odd minute offset is deliberate. If I have any other routine that drives a
browser, do not let the two schedules collide. Routines that share a single
browser profile deadlock when they overlap: both runs reach for the same
profile, one takes the session, the other waits for a session that is never
released, and both eventually time out having done nothing. Keep at least
thirty minutes between the start of this routine and the start of any other
browser routine, and do not schedule anything on the exact hour, since that is
where most schedules cluster by default. If a run finds the profile busy, do
not queue behind it and do not open a second profile. Skip that run, say so in
the report, and wait for the next slot.

### Where to look

Sweep the sources listed here, in this order, and stop when the list is done:

1. `<job board 1>`
2. `<job board 2>`
3. `<company careers page 1>`
4. `<company careers page 2>`

Replace these with real sources before the first run. Four to six is a healthy
list. Prefer sources that show a genuine posting date over ones that only show
a relative age. If a source is unreachable or has changed its layout so that
you cannot read dates reliably, skip it, note it in the report, and carry on
with the rest. Never guess at a field you could not actually read. Respect each
site's terms and robots rules, keep the request rate polite, and do not attempt
to get around a login wall, a paywall or a bot check.

### The filter hierarchy

This is the core of the routine. Three gates, applied in strict order:

**1. Fit. 2. Verified freshness. 3. Applicant count.**

Each gate can only reject. No gate can rescue. If a posting fails gate one,
gates two and three are never run on it, and a spectacular result at a later
gate never overturns an earlier rejection. Do not average the gates, do not
score them, and do not let two weak passes add up to a strong one.

**Gate 1: fit.** The posting has to be about `{{ROLE_KEYWORDS}}`, in one of
`{{LOCATIONS}}`, and free of every term in `{{EXCLUDE_KEYWORDS}}`. Read the
body of the posting, not the title. Titles are marketing. Reject anything where
the responsibilities describe a different job from the one the title advertises,
anything requiring a qualification or licence I do not have, and anything from a
recruiter that will not name the employer. If fit is genuinely ambiguous after
reading the body, reject it and list it under "close calls" in the report rather
than applying.

Fit outranks competition, and it is worth being explicit about why. Being the
second applicant for a role you will not get is worth less than being the
hundredth for one that matches you. Applicant count tells you the size of the
crowd. It tells you nothing about your standing inside it. A routine that sorts
by scarcity of competition will reliably fill the day with fresh, uncontested,
irrelevant work.

**Gate 2: verified freshness.** The posting must be no older than
`{{MAX_POSTING_AGE_DAYS}}` days, and the date must be one you verified rather
than one you read off a badge. Open the posting itself. Look for an explicit
posted or updated date in the body, in the page metadata, or in the application
form. If the listing carries a reference or requisition number, a number far out
of sequence with the site's current postings is a strong hint that the listing
is a repost. Where the only date available is a relative label, treat that label
as unverified and go looking for a real one. If you cannot establish a date
after a reasonable look, reject the posting. An absent date is a skip, not a
pass.

**The promoted listing trap.** A promoted, sponsored, boosted or reposted
listing can show a recent date while being weeks old underneath. Sites refresh
the visible timestamp when an employer pays to re-promote a listing, or when a
posting is closed and immediately reopened, and the badge then reports the
promotion date rather than the date the role opened. This is the single most
common way a job routine wastes a day: it applies to a pile of "posted 2 hours
ago" listings that have already been through fifty candidates. So freshness has
to be verified, never read off the badge. If the visible age and the evidence in
the body disagree, believe the body. If a listing is explicitly marked as
promoted and you cannot find a corroborating date anywhere, treat it as older
than `{{MAX_POSTING_AGE_DAYS}}` and reject it.

**Gate 3: applicant count.** Where a site shows how many people have applied,
use it as a tiebreaker when the surviving list is longer than
`{{APPLICATIONS_PER_SITE_PER_DAY}}`, preferring the lower counts. That is all it
is for.

Do not set a hard cap here, and be sceptical of one if you inherit it. An
applicant count cap set too low does not select for good roles, it selects for
recently posted ones, because every posting starts at zero applicants and climbs
from there. Filter hard on a low count and you have rebuilt gate two badly,
with the promoted listing trap left wide open. The count is also missing
entirely on many sites and estimated on others, so a hard cap silently rejects
every source that does not publish the number. Treat it as a soft secondary
signal that is only visible in some places, and let it break ties rather than
make decisions.

### State

Keep one durable table, carried between runs, with one row per application:

| Column | Notes |
| --- | --- |
| `date` | The date you submitted. |
| `company` | Employer, not the recruiter or the board. |
| `role` | Title as posted. |
| `link` | Canonical posting URL. |
| `source` | Which site it came from. |
| `outcome` | `applied`, `blocked`, `escalated`, `rejected`, `interview`, `offer`, `withdrawn`, `no reply`. |

Check this table before every application. If the same company and role appear
already, skip it and do not apply again, even from a different board, even if
the link is different. Reposts of a role I already applied to are duplicates,
not new opportunities. Update the `outcome` column when I tell you an
application has moved on, so the table stays a picture of the funnel rather than
a list of submissions. Alongside it, keep a lighter log of rejections with the
posting, the gate that rejected it and the reason, so the filters are auditable
and I can see whether a gate is throwing away things it should not.

### What to send

At the end of each run, send one message to `{{NOTIFY_CHANNEL}}` with:

- What you applied to: company, role, link, one line on why it fit.
- Close calls: what nearly qualified and which gate stopped it.
- Counts per gate, so I can see the shape of the funnel: postings seen,
  rejected on fit, rejected on freshness, deprioritised on applicant count,
  applied.
- Anything that needs me, at the top, flagged clearly.
- Anything that broke: sources that were unreachable, layouts that changed,
  a busy browser profile.

One message per run. Do not send a message per posting.

### When to stop and ask

Stop and hand it to me, rather than deciding yourself, whenever an application
requires any of:

- A cover letter, or any free text answer about motivation, fit or background.
- A salary expectation, current salary, or a rate.
- A notice period, start date or availability commitment.
- A visa, work authorisation, sponsorship or right to work answer.
- A personal document: CV variant, portfolio, references, certificates, ID,
  photo, or anything else uploaded from my files.
- An account creation, a login, a code sent to my phone or email, or a bot
  check.
- Any assessment, timed test or scheduling step.
- Anything that charges money or asks for bank or payment details.

In those cases, save the posting, mark the row `escalated`, and put it at the
top of the run report with what specifically is needed. Do not invent an answer,
do not reuse an answer from a previous application unless I gave you that answer
for reuse explicitly, and do not submit a partial application to hold a place.

### Do not

- Do not apply twice to the same company and role. Check the state table first.
- Do not exceed `{{APPLICATIONS_PER_SITE_PER_DAY}}` on any site in a day, and do
  not carry an unused allowance forward into tomorrow.
- Do not treat the cap as a quota to fill. Applying to nothing is a valid run.
- Do not let a later gate rescue a posting an earlier gate rejected.
- Do not trust a promoted or reposted badge as evidence of freshness.
- Do not apply to a posting whose date you could not verify.
- Do not write anything in my voice, or state anything about my experience,
  availability or expectations that I have not given you.
- Do not create accounts, accept terms, or agree to a background check on my
  behalf.
- Do not work around a bot check, a login wall or a rate limit, and do not
  reduce the interval between requests to get more done.
- Do not open a second browser profile because the first one is busy.
- Do not silently drop a source that failed. Report it.
- Do not delete or rewrite state history. Append, and update outcomes in place.

---

If you also want it as a workflow, the same method is available as an n8n
workflow: `job-search.n8n.json`.
