AI Automation
the engine
On an AI system that acts on my own LinkedIn account, and why the guardrails live in code and not in good intentions
Most AI demos die as a pilot. Not because the tech falls short, but because nobody dares to let the system actually do anything. The demo runs in a sandbox, everyone nods, and then a human gets slotted between every action. That human becomes the bottleneck, the business case evaporates, and six months later the project is “on hold”.
I flipped it around. On my own, I built a LinkedIn engine that runs in production 24 hours a day, on my own account. Not a test account, not behind an approval button. When someone comments on one of my posts with a trigger word, Claude writes a personal message, and the engine sends it automatically, with the promised PDF guide attached. No human looks in between anymore.
That sounds reckless. The opposite is true, and that’s exactly the point of this piece.
what’s running
The stack is deliberately boring. Python with Playwright for the browser actions, Supabase as the data layer, Make for orchestration, the Claude API for the writing, and a Hetzner VPS at around five euros a month with systemd as the process manager. No Docker, no Kubernetes, no framework of the month.
The architecture has one hard rule: the AI logic knows nothing about LinkedIn, and the LinkedIn actions know nothing about Claude. The browser actions sit in a reusable library of atomic functions. The production scripts are dumb: import, loop, log. All the intelligence lives in one place, all the execution in another. That’s what makes the system testable, and it’s what makes the next modules possible without a rebuild.
valves, not trust
The system is allowed to send automatically precisely because I don’t trust it. That’s not a contradiction, that’s the design choice.
Every action passes through valves that live in code, not in a promise to myself. An activity window: actions only between 07:00 and 23:00 Dutch time. A daily cap on sent messages, enforced hard in the script itself, not just in the orchestration layer. A validation gate that checks every generated message for length, language, personalization and formatting before anything goes out the door. A check that the promised PDF actually exists. Random delays between every action, three to eight seconds between page loads, at least three minutes between messages. And a dry-run mode that’s on by default, so sending is an explicit decision.
The difference with the average pilot: there the safety lives in a human who watches along. Here the safety lives in code that doesn’t get tired, doesn’t get distracted and isn’t in a hurry. A human as a guardrail doesn’t scale. An if-statement does.
the evening of May 21
And yet it went wrong. On May 21 I watched in the live logs as the engine sent the same commenter a public reply for the third time. And then once more.
The cause was a design mistake that looked perfectly reasonable in review. To keep a comment from being processed twice, every comment needed a dedup key. That key was a hash of the scraped comment text. Sounds logical: same text, same comment.
Except: the scraped text also included the visible reply and reaction counters. “WIKI 0” became “WIKI 0 1 1” the moment someone reacted. And who reacted? My own engine. The auto-reply bumped the counter, the counter changed the text, the text changed the key, and to the system it was suddenly a new comment. One that deserved a reply. One that bumped the counter again.
A loop that fed itself, with my name under it, publicly visible on LinkedIn. I only caught it because I happened to be watching live that evening. The DM side didn’t get stuck in that loop, and that’s the most instructive detail: sending a DM changes nothing about the comment it’s based on. A reply does. The action that mutates its own input, that’s the trap.
keys that must not move
The fix was small and the principle is big. The dedup key now hangs on post plus author, two things the engine itself can never change. And a new valve came with it: a hard daily cap on replies, so that even a loop I don’t see runs into a wall on its own instead of into my reputation.
This is the lesson no demo teaches you. A demo only has to work once while everyone’s watching. A production system has to behave while nobody’s watching, even when it changes its own environment and then gets that changed environment back as input. Idempotency is a boring word in a slide deck. In production it’s the difference between a system and an incident.
Let’s be honest about where this stands: it runs, but it isn’t finished. This is module one of three, and LinkedIn gave me a warning along the way about third-party tooling. My answer wasn’t to stop but to dial back: the volumes are now set to five messages a day, a fraction of what the system can handle. That’s not a defeat, it’s the same philosophy. The valve lives in code, so dialing back is one line of config.
What I’ve taken away from this is a simple test for any automation plan, mine or a client’s. Not: what can the system do? But: what happens when the system runs into its own tracks, and which line of code stops it then? Anyone who can’t answer that question doesn’t have a production system. They have a demo that hasn’t been allowed to fail yet.