Public project
LinkedIn Automator
Most outreach tools optimize for volume. This one is built around the constraint that actually matters: the account being automated belongs to a real person, and a restricted profile costs more than any campaign is worth. Campaigns are assembled from action blocks, but every block carries a guard that cannot be removed, so a sequence stops when someone replies instead of talking over them. It runs in the operator’s own signed-in Chrome, keeps separate state per account, and resumes a schedule correctly after the machine has been closed for hours.
GitHub repositoryContext
A lot of outreach tools optimize for volume first. I wanted to think from the opposite direction.
The account being automated belongs to a real person. A restricted profile costs more than any campaign is worth, so the limits are part of the design rather than a settings page nobody opens.
Local-first execution keeps the work inside the browser session the operator already uses, rather than a datacenter address that looks nothing like their normal activity.
This one is earlier than the CRM agent. I have run it end to end on my own account, and the team has not adopted it yet.
How it works
Each LinkedIn account is its own workspace with its own persistent Chrome profile, signed in once and reused by later runs.
Campaigns are built from action blocks, and every action is paired with an automatic guard: wait for acceptance, or check for replies before following up.
Before it acts on a lead it opens that person’s profile and reads their name, company, title, location, and industry off the page into variables, so a message resolves per person rather than per template.
Replies are watched on a timer while the app is open and checked again at the moment of sending, and a reply raises a notification so the conversation gets a human rather than another scheduled message.
A daily ceiling, per-action limits, working hours, and randomized delays pace the run so it does not behave like a script.
Schedule state is written down rather than held in memory, so a delay measured in days survives the app being closed.
Zoho CRM is wired in as the external CRM, since that is what the team already runs.
Not looking like a script
The hard part of this project was never assembling the actions. It is that LinkedIn actively looks for automation, and being spotted does not cost you a failed run, it costs you the account. Every design decision downstream of that is shaped by it.
So the system works the way a person does. It drives the operator’s own signed-in Chrome from their own connection rather than a headless browser on a datacenter address. It moves the cursor, scrolls the page, and pauses as though reading before it does anything. Typing has a variable rhythm, and it will sometimes hit a wrong key and backspace to fix it, because a flawless input stream at a constant cadence is itself the tell. Delays between actions vary rather than repeating, and nothing runs outside the hours the operator has set.
None of this is a feature anyone would ask for in a spec, and it took longer to get right than the campaign runner it protects. On the account I run it on, it has not been flagged or restricted.
The guard is part of the action
Every sequence builder I have used treats stop on reply as a checkbox. It sits in a settings panel, it is off by default or someone forgets it, and the result is a person who answered politely getting three more scheduled messages.
Here the guard is attached to the action rather than to the campaign. A connection request is followed by a wait for acceptance, and only accepted leads move on. A message is followed by a reply check, and a reply ends the sequence and moves that lead to the inbox. Removing the guard means removing the action it belongs to.
Replies are caught twice over. A timer refreshes conversations while the app is open, so an answer surfaces as a notification within minutes of arriving rather than whenever the next action happens to be due. The check then runs again at the moment of sending, as the hard stop. The first layer is so a person knows; the second is so the system cannot get it wrong.
The point is that the careless version of the workflow is the safe one. You have to work quite hard to build something rude with it, which is the right default for a tool acting under someone’s own name.
Resuming a schedule that outlived the session
A follow-up scheduled five days out has to survive the machine being shut down, and almost all of the difficulty is in what happens between sessions rather than during them.
The system records when each message was actually sent instead of counting down in memory. On start it reads that timestamp and works out what is owed: if the delay was sixty minutes and thirty have passed, it waits the remaining thirty; if the app was closed for longer than the delay, the message is already due and goes out on the next pass.
The failure I was designing against is the one where somebody comes back from a weekend and finds four messages that all arrived at once. Getting that arithmetic right across restarts took longer than building the campaign runner it protects.
Challenges
Behaving enough like a person that LinkedIn never treats the session as automation, which was by far the hardest part of the build.
Resuming a schedule after the app has been closed, without a lead receiving every overdue follow-up at once.
Keeping browser state, campaign state, and message state aligned across sessions.
Reading fields off profile pages that do not all have the same shape, so a message never goes out with an empty company or title in it.
Writing public-facing docs that explain the project without making it sound like spam tooling.
A CRM block exists in the workflow builder but is disabled while a bug is fixed, so it does not appear in these screenshots.