<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Automation on hippotion</title><link>https://blog.hippotion.com/tags/automation/</link><description>Recent content in Automation on hippotion</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 12 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.hippotion.com/tags/automation/index.xml" rel="self" type="application/rss+xml"/><item><title>Two Birds That Read the Web for Me: One Hoards, One Scatters</title><link>https://blog.hippotion.com/posts/two-birds-read-the-web/</link><pubDate>Fri, 12 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/two-birds-read-the-web/</guid><description>I gave my second brain two agents that read the outside world and collide it against my notes. A Magpie watches my GitHub stars and only speaks when something hits live work. A Blue Jay reads a handful of RSS feeds and surfaces the distant, not-yet-relevant connection. They share a security spine — and they have deliberately opposite jobs. Here&amp;rsquo;s why the split is the whole design.</description><content:encoded><![CDATA[<p>I have a <a href="/posts/a-second-brain-you-can-git-clone/">vault of markdown notes</a> that
I treat as a second brain, and I <a href="/posts/gitops-for-my-brain/">run GitOps over it</a>
like it&rsquo;s production infrastructure. It already has agents that work on it from
the <em>inside</em>: a <a href="/posts/an-ai-gardener-for-your-second-brain/">nightly gardener</a>
that weeds orphans and suggests links, and a Wanderer that collides random pairs
of my own notes looking for connections I missed.</p>
<p>The obvious next move is to point an agent at the <em>outside</em> — let it read the web
and tell me what matters. That move is also a small landmine, and most &ldquo;AI reads
the internet for you&rdquo; tooling steps right on it. So this week I built two of them
instead of one, named them after corvids, and the reason there are two is the
entire point of this post.</p>
<p>Meet the <strong>Magpie</strong> and the <strong>Blue Jay</strong>.</p>
<h2 id="the-same-fear-twice">The same fear, twice</h2>
<p>Before either bird got a name, both inherited a single non-negotiable rule, and
it&rsquo;s worth saying plainly because it&rsquo;s the part everyone skips:</p>
<blockquote>
<p>An agent that reads the internet and writes to your notes is a prompt-injection
pipeline aimed straight at your trust root.</p>
</blockquote>
<p>My vault isn&rsquo;t just storage. Every <em>other</em> agent — the gardener, the Wanderer,
the search that answers &ldquo;what am I building?&rdquo; — reads it as <strong>trusted context</strong>.
So the moment one agent ingests a GitHub README or a news headline (attacker-
influenceable text) and is allowed to write a note, a stranger on the internet
gets to whisper instructions into the thing my whole system believes. &ldquo;Structured
API&rdquo; narrows that surface. It does not close it.</p>
<p>Both birds are built on the same chassis as the gardener, and that chassis
<em>enforces</em> the fear rather than trusting the model to behave:</p>
<ul>
<li><strong>Two phases, hard split.</strong> A wrapper-owned <strong>FETCH</strong> step pulls the external
text in plain Bash — Claude is not in the loop, can&rsquo;t be talked into anything,
because it isn&rsquo;t running yet. Then a <strong>COLLIDE</strong> step starts <code>claude -p</code> with
the fetched text handed in as inline <em>data</em>, and that process gets only
<code>Read</code> / <code>Glob</code> / <code>Grep</code> / <code>Write</code>. <strong>No Bash, no git, no network, no MCP.</strong>
While untrusted text is in the context window, the agent has no tool that can
reach the outside world or rewrite history.</li>
<li><strong>Allowlist, not the open web.</strong> Each bird reads a short, named list of
sources. Nothing else.</li>
<li><strong>Quarantine, not the vault.</strong> Findings land in <code>quarantine/&lt;bird&gt;/</code>, which
lives <em>outside</em> <code>vault/</code>. The indexer never sees it. Nothing it writes is ever
auto-wikilinked into the graph. Promotion to a real note is a thing <strong>I</strong> do,
by hand, after reading it.</li>
<li><strong>Blast radius is checked, not assumed.</strong> A run may modify <em>only</em> its quarantine
directory. Anything written anywhere else is discarded and reported as a
<code>violation</code>.</li>
<li><strong>&ldquo;Nothing found&rdquo; is a successful run.</strong> Neither bird has a quota. This is the
honesty contract I stole from the Wanderer — an agent under pressure to produce
N findings will manufacture N findings, and manufactured insight is worse than
silence.</li>
</ul>
<p>That&rsquo;s the shared spine. Now the interesting part: given the <em>same</em> security
model, the two birds do almost opposite things, and trying to make one bird do
both jobs would have quietly ruined it.</p>
<h2 id="the-magpie-hoards-whats-already-shiny">The Magpie hoards what&rsquo;s already shiny</h2>
<p>A magpie collects shiny objects and keeps them close. Mine watches <strong>my own
GitHub stars</strong>.</p>
<p>The premise is <em>slow public signal × private context</em>. I starred some repo three
weeks ago, forgot about it, and moved on. Meanwhile my projects shifted. The
Magpie runs weekly, pulls my starred repos through one allowlisted endpoint
(<code>gh api user/starred</code>), and collides each one against what I&rsquo;m <strong>actively
building right now</strong> — the live projects, the open hubs.</p>
<p>Its output contract is a tight one: it is a <strong>relevance filter</strong>. It fires <em>only</em>
when a star actually touches live work, and every finding has to name three
concrete things — the repo, the project it connects to, and one &ldquo;so what.&rdquo; A
vague &ldquo;these are thematically related&rdquo; doesn&rsquo;t count as a hit. It&rsquo;s a watchdog on
the dials, not a newsletter.</p>
<p>The supervised proof run, over 28 stars, surfaced exactly two real hits and
refused to invent a third:</p>
<ol>
<li><strong><code>supertonic</code> (on-device multilingual TTS)</strong> × my
<a href="/posts/clone-your-voice-hungarian-audiobooks/">Hungarian-audiobook voice-cloning project</a>
— a possible escape from a TTS fight I&rsquo;d been losing. I checked: it genuinely
supports Hungarian. That&rsquo;s a hit with a so-what.</li>
<li><strong><code>agentmemory</code></strong> × the exocortex itself — prior art for persistent AI memory,
notably <em>with benchmarks</em> my own notes lacked. (And if you&rsquo;ve read about
<a href="/posts/graph-hurt-my-search/">the time I benchmarked my own search and it lost</a>,
you&rsquo;ll know how much I needed that nudge.)</li>
</ol>
<p>The other ~22 stars mapped to tidy thematic clusters and were correctly <em>not</em>
reported. That restraint is the feature.</p>
<h2 id="the-blue-jay-scatters-acorns-and-forgets-where">The Blue Jay scatters acorns and forgets where</h2>
<p>Here&rsquo;s the bird that explains why there are two.</p>
<p>Blue jays don&rsquo;t hoard close like magpies. They <strong>cache acorns far and wide and
forget where they buried some</strong> — and the forgotten ones grow into oak trees.
Ecologists think blue jays are why oak forests spread north after the last ice
age. Seed dispersal, by way of a bad memory. That is <em>exactly</em> the job I wanted
for the second bird, and the metaphor was too good to pass up.</p>
<p>The Blue Jay reads an allowlist of <strong>eight RSS feeds</strong>, picked so tech and science
cross-pollinate:</p>
<ul>
<li><strong>Tech:</strong> Hacker News (high-score front page), lobste.rs, Ars Technica</li>
<li><strong>Science &amp; ideas:</strong> phys.org, Quanta, Aeon, Nautilus</li>
<li><strong>Wildcard:</strong> Medium — but scoped to specific <em>tag</em> feeds, never the raw
firehose of crypto and self-help</li>
</ul>
<p>Quanta, Aeon, and Nautilus are on that list on purpose: they&rsquo;re the connective
tissue, the feeds where &ldquo;huh, that&rsquo;s weirdly similar to&hellip;&rdquo; happens before my
vault even gets involved.</p>
<p>And its output contract is the <strong>opposite</strong> of the Magpie&rsquo;s. The Blue Jay is a
<strong>serendipity filter</strong>. Its job is to surface the connection that <em>isn&rsquo;t</em> in my
projects yet — the distant idea, the acorn worth burying. If I ran it through the
Magpie&rsquo;s &ldquo;only fire on a live-work hit&rdquo; rule, I would strangle the one thing it
exists to do. Relevance and serendipity pull in opposite directions, and you
can&rsquo;t tune a single agent to maximize both.</p>
<p>One more load-bearing detail, half design and half security: the Blue Jay
<strong>collides on the RSS summary only</strong> — title, abstract, link. It never pulls the
full article body into context. That&rsquo;s simultaneously the lower-injection path
<em>and</em> the right cognitive shape (a headline is a seed; I click through myself from
quarantine if the seed is interesting). The narrow input is doing double duty.</p>
<h2 id="why-two-birds-and-not-one-with-a-flag">Why two birds and not one with a flag</h2>
<p>I genuinely considered making this one agent with a <code>--mode=relevance|serendipity</code>
switch. I&rsquo;m glad I didn&rsquo;t, and the reasoning generalizes past birds:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th><strong>Magpie</strong></th>
					<th><strong>Blue Jay</strong></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Source</td>
					<td>my GitHub stars (structured API)</td>
					<td>8 RSS feeds (open prose)</td>
			</tr>
			<tr>
					<td>Injection risk</td>
					<td>low</td>
					<td>the highest frontier</td>
			</tr>
			<tr>
					<td>Fires when</td>
					<td>a star hits <strong>live work</strong></td>
					<td>a summary sparks a <strong>distant</strong> idea</td>
			</tr>
			<tr>
					<td>Output</td>
					<td>relevance: repo → project → so-what</td>
					<td>serendipity: the not-yet-relevant connection</td>
			</tr>
			<tr>
					<td>Failure mode it guards against</td>
					<td>noise / false relevance</td>
					<td>being strangled into silence</td>
			</tr>
	</tbody>
</table>
<p>Two things made the split non-negotiable. First, <strong>the output contracts are too
different to share one brain</strong> — &ldquo;only speak on a hit&rdquo; and &ldquo;speak about the thing
that isn&rsquo;t a hit yet&rdquo; are contradictory prompts, and a single agent told to do
both does neither well. Second, <strong>open news is a higher injection frontier than a
structured stars API</strong>, so the riskier bird deserves its own enforced blast-radius
wrapper, not a code path bolted onto the safe one. When two jobs disagree on both
<em>what good output is</em> and <em>how dangerous the input is</em>, that&rsquo;s not a flag. That&rsquo;s
two programs.</p>
<p>So now my vault has two more agents reading the world on a cron. The Magpie runs
Saturday at 06:00 and tells me when something I bookmarked finally became
relevant. The Blue Jay runs Saturday at 07:00 and buries acorns in a quarantine
folder, most of which I&rsquo;ll ignore — but I only need one of them to grow into an
oak.</p>
<p>Both are on probation for their first few runs, because I don&rsquo;t trust a thing that
reads the internet until I&rsquo;ve watched it behave. But the part I&rsquo;m actually happy
about isn&rsquo;t the agents. It&rsquo;s that building the <em>second</em> one forced me to say out
loud what the first one was secretly assuming — and the names made the difference
impossible to forget. A magpie hoards. A blue jay scatters. You want both, and
you do not want them to be the same bird.</p>
]]></content:encoded></item><item><title>Mind the gap: I pointed monitoring at my own skill set</title><link>https://blog.hippotion.com/posts/mind-the-gap-skill-radar/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/mind-the-gap-skill-radar/</guid><description>A rejection isn&amp;rsquo;t actionable data. So an n8n workflow now extracts skill demand from live job listings, diffs it against what I can prove, and renders the gap as a dashboard — deployed like everything else here: via git push.</description><content:encoded><![CDATA[<p>A while back I applied for a senior platform role at n8n and didn&rsquo;t land it. Fair enough — but
&ldquo;fair enough&rdquo; isn&rsquo;t actionable. Rejections come with no logs, no metrics, no trace. For someone
who runs thirty-odd services with full observability, having <em>vibes</em> as the only instrumentation
on my own career felt architecturally embarrassing.</p>
<p>So I built <strong>mind-the-gap</strong>: a pipeline that measures what the market demands, diffs it against
what I can prove, and renders the gap as a private dashboard on my cluster. The job hunt is now a
monitored system. This post is about the non-obvious decisions.</p>
<h2 id="demand-an-llm-reads-job-listings-so-i-dont-have-to">Demand: an LLM reads job listings so I don&rsquo;t have to</h2>
<p>I already had <a href="/posts/ats-job-poller/">a job poller</a> — an n8n workflow that polls the public ATS
APIs (Greenhouse / Lever / Ashby) of ~33 companies plus a broad remote-jobs feed every six hours.
A sibling workflow now re-fetches the same boards and, for every listing that passes the
role+location gate, asks a small hosted LLM (Llama-3.1-8B) for a structured extraction:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span><span class="nt">&#34;seniority&#34;</span><span class="p">:</span> <span class="s2">&#34;senior&#34;</span><span class="p">,</span> <span class="nt">&#34;skills&#34;</span><span class="p">:</span> <span class="p">[{</span><span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;kubernetes&#34;</span><span class="p">,</span> <span class="nt">&#34;importance&#34;</span><span class="p">:</span> <span class="s2">&#34;must&#34;</span><span class="p">},</span> <span class="err">...</span><span class="p">]}</span>
</span></span></code></pre></div><p>One row per <em>(job, skill)</em> lands in an n8n Data Table. Decisions that mattered:</p>
<ul>
<li><strong>One LLM call per job, not one batch.</strong> Free-tier inference times out on batches; per-job calls
are slower but fail independently. A lesson the poller already paid for.</li>
<li><strong>Insert doubles as the processed-marker.</strong> A job whose extraction fails to parse produces no
rows — so it&rsquo;s retried next run, for free. No status column, no second table.</li>
<li><strong>Canonicalization in code, not in the prompt.</strong> The model says &ldquo;K8s&rdquo;, &ldquo;k3s&rdquo;, &ldquo;EKS&rdquo; on
different days regardless of instructions. A dumb alias map (<code>k8s→kubernetes</code>, <code>eks→aws</code>)
beats prompt engineering for consistency.</li>
<li><strong>8B is good enough — with a guard.</strong> It occasionally echoed the seniority enum back literally
(<code>&quot;junior|mid|senior|staff|lead|unspecified&quot;</code>). The fix is one line of validation, not a bigger
model.</li>
</ul>
<h2 id="supply-no-artifact-no-credit">Supply: no artifact, no credit</h2>
<p>The other side of the diff is a skills registry — markdown in my knowledge vault, with a
machine-parseable YAML block. Every skill has a state, and the rule that keeps the whole thing
honest is brutal: <strong>a skill counts as <code>proven</code> only if an artifact exists</strong> — a public repo, a
blog post, documented production experience. Otherwise it&rsquo;s <code>claimed</code>, and claimed earns half
credit.</p>
<p>That rule immediately produced the most useful insight of the project: <strong>&ldquo;invisible skill&rdquo; is a
real category.</strong> Python turned out to be the market&rsquo;s #5 ask. I use it constantly — and could
point to nothing public that shows it. The cheapest score increase isn&rsquo;t learning something new;
it&rsquo;s a weekend making an existing skill visible. No gut-feeling gap analysis would have ranked
&ldquo;write about what you already do&rdquo; above &ldquo;learn the shiny thing.&rdquo;</p>
<h2 id="the-score-distinct-companies-not-mentions">The score: distinct companies, not mentions</h2>
<p>First naive aggregation: Canonical&rsquo;s listings mention Ubuntu <em>nine times, all marked must-have</em> —
suddenly Ubuntu looks like the hottest skill in Europe. Employer skew is the noise floor of small
samples. The fix: demand weight = <strong>distinct companies naming the skill</strong>, not total mentions.
One enthusiastic employer can&rsquo;t move the radar.</p>
<p>Two more scoring rules I&rsquo;d defend in review:</p>
<ul>
<li>Skills named by fewer than two companies don&rsquo;t count at all — single-listing noise stays out.</li>
<li>Demand the registry hasn&rsquo;t classified yet shows up as &ldquo;unreviewed&rdquo; and <strong>counts fully against
the score</strong>. An unreviewed market signal is a gap until proven otherwise; the dashboard nags me
to triage it.</li>
</ul>
<h2 id="rendering-the-page-is-a-git-commit">Rendering: the page is a git commit</h2>
<p>The dashboard is a single static HTML file, and the pipeline that produces it never touches the
cluster. <code>render.js</code> lives in this repo as the single source of truth; a nightly n8n workflow
fetches it raw from GitLab, <code>eval()</code>s it against the Data Table rows and the registry, and — only
if the result differs from what&rsquo;s committed (timestamps stripped, or every night is a &ldquo;change&rdquo;) —
PUTs the new <code>index.html</code> back via the GitLab API.</p>
<p>Serving is the same pattern as this blog: nginx plus a git-pull sidecar, deployed by Argo CD,
behind the cluster&rsquo;s OAuth middleware. The renderer has no kubeconfig, no SSH, no cluster access
of any kind. <strong>GitLab stays the only source of truth — even for a page that rewrites itself
nightly.</strong> If the workflow goes rogue, the worst it can do is a reviewable commit.</p>
<h2 id="day-one-verdict">Day-one verdict</h2>
<p>First run: 2,297 postings fetched, 25 in scope, 257 skill rows. Coverage score: <strong>63%</strong>.
Kubernetes and AWS tied at the top of demand — which means the AWS gap-closing project already in
flight stopped being a hunch and became the measured top of the market. Go is the only top-ten
demand with zero supply. The dashboard doesn&rsquo;t get anyone a job; it just makes sure every learning
Saturday is pointed where the data says, not where the hype does.</p>
<p>The job board rejected me. The data didn&rsquo;t.</p>
<hr>
<p><em>Workflows, render.js, and setup: <a href="https://github.com/janos-gyorgy/mind-the-gap">github.com/janos-gyorgy/mind-the-gap</a>.</em></p>
]]></content:encoded></item><item><title>🌙 Killing Mildew in the Dark</title><link>https://blog.hippotion.com/posts/killing-mildew-in-the-dark/</link><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/killing-mildew-in-the-dark/</guid><description>A farm robot is replacing pesticides with UV light at night. The clever part isn&amp;rsquo;t the robot — it&amp;rsquo;s the darkness. Here&amp;rsquo;s the home version, and the honest scope of what it can and can&amp;rsquo;t do.</description><content:encoded><![CDATA[<p>I saw a clip of an autonomous farm robot — TRIC Robotics — driving strawberry beds in total
darkness, killing pathogens with UV light instead of spraying them. Zero chemicals, zero runoff.
My first reaction was &ldquo;that&rsquo;s a marketing robot.&rdquo; My second, after reading, was &ldquo;no, the science
is real — and the robot is the least interesting part.&rdquo;</p>
<p>The interesting part is <em>why it works at night.</em></p>
<h2 id="the-trick-is-the-darkness-not-the-light">The trick is the darkness, not the light</h2>
<p>UV-C light (254 nm) shreds the DNA of fungal pathogens like powdery mildew. Nothing new there —
it&rsquo;s the same wavelength that sterilises water and hospital rooms. The problem is that in daylight
those pathogens <em>repair</em> the damage, using a light-activated enzyme (photoreactivation). Zap them
at noon and they patch themselves up by evening.</p>
<p>So you do it in the dark. With the repair pathway switched off, a tiny dose sticks. Cornell&rsquo;s
Gadoury lab spent years on this: nighttime UV-C at doses around <strong>85 J/m² once a week</strong> gave
season-long powdery mildew control on strawberries that <em>beat the best available fungicides</em>.
Grapes, cucumbers, roses — same story. Applied about 30 minutes after sunset, finished within a
couple of hours.</p>
<p>That&rsquo;s a genuinely beautiful result. Not a new chemical, not a stronger lamp — just the same old
light, applied when the enemy can&rsquo;t fix itself.</p>
<h2 id="what-it-is-and-what-it-absolutely-isnt">What it is, and what it absolutely isn&rsquo;t</h2>
<p>Before anyone rips out their whole garden routine: this is <strong>not</strong> a general pesticide replacement.
The evidence is strong for one specific class of problem — surface fungal pathogens, mostly
<strong>powdery and downy mildew</strong> on susceptible plants (strawberry, grape, cucurbits, roses). It does
nothing for slugs, most insects, or anything in the soil.</p>
<p>So the honest pitch is narrow: <em>if you fight recurring mildew every summer, this is a chemical-free
tool that genuinely works.</em> If your real enemy is aphids, don&rsquo;t build this — you&rsquo;d be solving the
wrong problem with a dangerous toy.</p>
<p>Which brings me to the toy being dangerous.</p>
<h2 id="the-part-where-i-tell-you-not-to-blind-yourself">The part where I tell you not to blind yourself</h2>
<p>UV-C is not mood lighting. Seconds of direct exposure burn your eyes (welder&rsquo;s-flash) and skin,
and it&rsquo;s a long-term cancer risk. This is the single reason a <em>home</em> version has to be designed
carefully — and the reason I&rsquo;d never run an exposed source in a garden where my kids play.</p>
<p>Any home rig needs, non-negotiably:</p>
<ul>
<li>A physical enclosure or skirt so the light only hits the bed, never a person.</li>
<li>A hard interlock — a motion sensor or door contact that cuts power instantly if anything moves
into range.</li>
<li>A schedule that only ever runs in the dead of night, when everyone&rsquo;s inside and asleep.</li>
</ul>
<p>You can also over-dose the <em>plants</em> — too much UV-C scorches leaves. The whole point is that the
effective dose is tiny, so more is not better.</p>
<h2 id="the-build-the-home-version-of-while-you-sleep">The build (the home version of &ldquo;while you sleep&rdquo;)</h2>
<p>You don&rsquo;t need TRIC&rsquo;s autonomous navigation. A home garden has <em>fixed beds</em> — so the robot problem
collapses into a much simpler one: get a shielded lamp over a known bed, for a known number of
seconds, at night. That&rsquo;s not robotics. That&rsquo;s a timer and a rail.</p>
<p>Here&rsquo;s the plan I&rsquo;d build:</p>
<ol>
<li><strong>The lamp.</strong> A low-pressure UV-C tube (254 nm — <em>not</em> the &ldquo;UV-C LED&rdquo; novelties, and <em>not</em>
ozone-generating 185 nm lamps). Mounted in a hooded reflector so the light points down and is
blocked from the sides.</li>
<li><strong>The geometry.</strong> Fix it at a set height over the bed — on a simple cart that rolls a track, or
just a static fixture over a raised bed. Fixed height = repeatable dose.</li>
<li><strong>The dose, measured not guessed.</strong> This is the one place you can&rsquo;t wing it: borrow or buy a
UV-C meter, measure the irradiance (W/m²) at canopy height, then <code>time = 85 ÷ irradiance</code>.
If the lamp delivers, say, 5 W/m² at the leaves, that&rsquo;s ~17 seconds of exposure. Seventeen
seconds, once a week. That tiny number is the whole reason this is plant-safe and low-energy —
and why a slow-moving robot pass is enough on a farm.</li>
<li><strong>The brain.</strong> This is the bit that&rsquo;s actually in my wheelhouse: an ESP32 + a relay, on the
homelab. Fires at 2 a.m. for N seconds, once a week. A PIR sensor wired as a kill-switch. A
<code>mind-the-gap</code>-style cron and a log line to my phone when it ran. The &ldquo;autonomous robot working
while you sleep&rdquo; headline, minus the $100k of autonomy I don&rsquo;t need for four raised beds.</li>
</ol>
<h2 id="verdict">Verdict</h2>
<p>I haven&rsquo;t built this yet — it&rsquo;s a someday project, parked here so I stop losing the idea. But it&rsquo;s
the rare someday project where the science is settled, the materials are cheap, and the only real
engineering is <em>safety and dose control</em>, both of which are squarely the kind of problem I like.</p>
<p>The farm robot&rsquo;s pitch is &ldquo;pesticide-free at scale.&rdquo; The home version&rsquo;s pitch is smaller and more
honest: <strong>if mildew is your summer tax, you can pay it in seventeen seconds of midnight light
instead of a spray bottle.</strong> I&rsquo;ll take that trade.</p>
<p>When I build it, the failure log gets its own post.</p>
]]></content:encoded></item><item><title>🎯 Know the Market Without Job-Hunting: An LLM-Scored Job Poller in n8n</title><link>https://blog.hippotion.com/posts/ats-job-poller/</link><pubDate>Fri, 13 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/ats-job-poller/</guid><description>You don&amp;rsquo;t have to be job-hunting to want to know your market — what&amp;rsquo;s out there, what it pays, where you&amp;rsquo;d fit. So I built an n8n workflow: it polls the public ATS APIs (Greenhouse/Lever/Ashby) plus a broad remote-jobs feed, filters for remote-EU infra roles, scores each posting against my CV with an LLM, and emails me only the 80%+ matches. No database, no scraping.</description><content:encoded><![CDATA[<p>You don&rsquo;t have to be about to change jobs to want to know the landscape. What&rsquo;s being built, what it
pays, where you&rsquo;d actually fit — staying current on the market (and your own worth) is just good
professional hygiene. The trouble is that <em>checking</em> is tedious, so most of us don&rsquo;t, until we&rsquo;re
already job-hunting and starting cold.</p>
<p>So I automated mine. An <a href="https://n8n.io">n8n</a> workflow on my homelab polls job boards every six hours,
scores each new posting against my profile with an LLM, and emails me only the strong matches — the
ones scoring 80%+. When it&rsquo;s quiet, it&rsquo;s silent. When something genuinely fits, I know the same day.
Here&rsquo;s what I learned building it. Repo at the bottom.</p>
<h2 id="three-apis-cover-most-of-the-market">Three APIs cover most of the market</h2>
<p>Company career pages look bespoke, but underneath, the vast majority run on one of three ATS — and
all three hand you the jobs as unauthenticated JSON:</p>
<ul>
<li><strong>Greenhouse</strong> — <code>boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true</code></li>
<li><strong>Lever</strong> — <code>api.lever.co/v0/postings/{token}?mode=json</code></li>
<li><strong>Ashby</strong> — <code>api.ashbyhq.com/posting-api/job-board/{token}?includeCompensation=true</code></li>
</ul>
<p>No scraping, no headless browser. You poll the API the page itself calls, normalize the three
shapes into one <code>{ company, title, location, remote, url, posted_at, description, external_id }</code>, and
you&rsquo;re done with the hard part.</p>
<h2 id="resolve-the-token-is-half-the-battle">&ldquo;Resolve the token&rdquo; is half the battle</h2>
<p>The naive assumption — <em>the token is the company name, and everyone&rsquo;s on one of the three</em> — is half
right. When I probed my initial wishlist, <strong>roughly half 404&rsquo;d everywhere</strong>: HashiCorp (now under
IBM → Workday), SUSE (SuccessFactors), Aiven (Teamtailor), Hugging Face. They&rsquo;re on a fourth or fifth
system entirely. The honest move was to ship the ~33 that actually resolve and leave the rest as
disabled config stubs. Verify before you trust a slug.</p>
<h2 id="dedup-without-a-database">Dedup without a database</h2>
<p>I didn&rsquo;t want to stand up Postgres just to remember which jobs I&rsquo;d already seen. n8n&rsquo;s <strong>Data Tables</strong>
handle it natively: a <code>seen_jobs</code> table, an <code>external_id</code> namespaced <code>{ats}:{company}:{id}</code>, and the
<code>rowNotExists</code> operation drops anything already recorded. State lives inside n8n, backed up with it.
Zero extra infrastructure.</p>
<p>The ordering matters: <strong>notify first, mark seen second.</strong> The insert only happens after the email
sends, so a failed send retries next run instead of silently swallowing a posting.</p>
<h2 id="the-location-filter-is-a-trap">The location filter is a trap</h2>
<p>My first version kept everything that wasn&rsquo;t explicitly US-based. The inbox filled with <em>&ldquo;Senior
Platform Engineer — Spain (Remote)&rdquo;</em> and <em>&quot;… — United Kingdom (Remote)&quot;</em>. Those aren&rsquo;t remote-for-me
— they&rsquo;re remote <em>if you live in Spain</em>. Useless from where I sit.</p>
<p>The fix was to invert the logic. Keep only three things:</p>
<ul>
<li>globally-remote / worldwide / anywhere,</li>
<li>pan-EU (EMEA / Europe / EU / EEA),</li>
<li>my own country.</li>
</ul>
<p>…and <strong>drop single-country remote</strong>, even EU ones. Region and home matches win over the country
deny-list, ambiguous locations are kept (a missed match is worse than one extra line to skim). That
one change cut the noise more than anything else.</p>
<h2 id="let-an-llm-read-the-actual-job">Let an LLM read the actual job</h2>
<p>Keyword + location filtering gets you a candidate list, but it can&rsquo;t tell a &ldquo;Platform Engineer&rdquo; who
herds Kubernetes from a &ldquo;Platform Engineer&rdquo; who owns a Figma design system. The job description can.</p>
<p>So the last step scores each new posting against my CV. My first version batched all of them into
<strong>one</strong> big LLM call — which promptly timed out on the free tier. The fix was the opposite: <strong>one
small call per job</strong>, which also means a single slow or rate-limited job never sinks the batch. Each
call asks a <a href="https://build.nvidia.com">NVIDIA NIM</a> model (Llama 3.1 8B, OpenAI-compatible) for one
number and a reason:</p>
<blockquote>
<p>Score this job 0–100 for fit against my profile. Return <code>{score, reason}</code>.</p>
</blockquote>
<p>That score is what lets me <strong>widen the net instead of narrowing it.</strong> On top of the curated company
list I pull a broad remote-jobs feed (every company, all categories); the cheap keyword + location
filters do the first pass, then I <strong>only email the roles scoring 80%+.</strong> Casting wide is fine when a
model is the bar at the door. A line ends up looking like:</p>
<blockquote>
<p><strong>92%</strong> — <em>Grafana Labs</em> — Senior Platform Engineer (Remote, EMEA) — <em>strong k8s/GitOps overlap</em> — link</p>
</blockquote>
<p>Scoring is fail-safe: if a call hiccups, that job is just skipped, and every posting gets marked seen
either way — so nothing re-scores forever, and a rare bad run never floods or stalls the inbox.</p>
<h2 id="the-unglamorous-bits-that-make-it-trustworthy">The unglamorous bits that make it trustworthy</h2>
<ul>
<li><strong>One bad source can&rsquo;t kill the run</strong> — every fetch is wrapped; failures become a <code>⚠️ N sources failing</code> footer so a company quietly changing ATS is visible, not invisible.</li>
<li><strong>A prime run</strong> seeds the table silently the first time, so I&rsquo;m not buried under every currently-open
role on day one.</li>
<li><strong>Everything tunable lives in one Config node</strong> — companies, keywords, location lists, the profile,
the model — so adding a company is a one-line edit, not a graph safari.</li>
</ul>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li>The &ldquo;scrape job boards&rdquo; problem mostly isn&rsquo;t a scraping problem — it&rsquo;s three public APIs and a
normalizer.</li>
<li>For personal automation, reach for the boring-but-correct primitive: native dedup state beats a
database you have to operate.</li>
<li>An LLM works best here as the <strong>bar at the door</strong>: cheap deterministic filters keep the candidate
set (and the cost) small, then the model gates on real fit — which is what lets you cast a wide net
without drowning in it.</li>
</ul>
<p>Workflow JSON, the full node-by-node breakdown, and setup notes:
<strong><a href="https://github.com/janos-gyorgy/ats-job-poller">github.com/janos-gyorgy/ats-job-poller</a></strong>.</p>
]]></content:encoded></item></channel></rss>