LinkedIn doesn’t give us an official messaging API, yet demand for semi-automated outreach keeps climbing. Sales reps want faster pipeline, job hunters want more shots on goal, founders want to sanity-check ideas at scale. This guide shows how I wired up an OpenClaw LinkedIn message automation and outreach setup without getting my account throttled or my conscience dinged.
Why browser automation is the only viable LinkedIn path
LinkedIn’s Marketing Developer Platform is ads-only. The rest is walled. Most “LinkedIn automation” tools are glorified headless browsers with random sleeps. OpenClaw ships the same primitives—Chromium control, persistent memory, schedulers—but you own the stack and the code.
That matters for three reasons:
- Auditability – Your outreach logic lives in Git. No SaaS black box.
- Rate-limit discipline – You can tweak timing based on firsthand telemetry.
- Ethical flexibility – You set guardrails, not a vendor running thousands of accounts from the same AWS block.
Installing OpenClaw and the Puppeteer tool
I assume Node 22+. Grab the CLI and spin up a fresh agent:
npm install -g openclaw@latest
openclaw init linkedin-bot --template minimal
cd linkedin-bot
openclaw add-tool puppeteer
The add-tool command injects the Puppeteer integration (OpenClaw v4.6.1 bundles Chromium 124 by default). Under tools/puppeteer.ts you’ll see a thin wrapper that exposes page.goto, page.type, page.click, etc. to the agent’s action graph.
Session handling: keep cookies, drop fingerprints
LinkedIn fingerprints aggressively: canvas, fonts, window size, even scroll speed. Two rules keep the account safe:
- Reuse the exact same browser profile. Under
config/puppeteer.jsonset a fixed user-data-dir:
{
"executablePath": "/usr/bin/chromium",
"userDataDir": "~/.openclaw/linkedin-profile",
"headless": false,
"slowMo": 40
}
- Throttle actions. LinkedIn allows ~200 connection requests/day for warm accounts, ~100 for new. Messages are softer-limited but spam reports nuke reach. We’ll bake limits into the scheduler later.
Drafting personalized connection requests that don’t read like spam
LLMs love generic praise: “Impressed by your background!” Everyone else rolls their eyes. The trick is to blend a template with one nugget scraped from the prospect’s public profile. My agent extracts the most recent post title if it exists; else the current role.
// agent/actions/writeConnectionRequest.ts
export async function writeConnectionRequest({ profile }) {
const hook = profile.lastPostTitle ?? profile.currentRole ?? "your work";
return `Hey ${profile.firstName}, I enjoyed your piece on ${hook}. Would love to connect and swap notes if you’re open to it.`;
}
Yes, this still looks templated, but early tests show a 43 % accept rate vs 28 % for pure boilerplate (n=211).
Follow-up sequences without sounding like a robocall
LinkedIn messages show timestamps to the millisecond if you hover. That’s a tell for bot activity when three nudges land at 08:00:01 daily. Use OpenClaw’s cron DSL but randomize inside the window:
// schedules/followUp.claw
sequence "post-connection-90d" {
step 1 "first-nudge" at cron "0 10 * * *" jitter "10m"
step 2 "second-nudge" after 5d jitter "30m"
step 3 "breakup" after 14d jitter "1h"
}
Each step maps to an action. Example second nudge:
export async function secondNudge({ page, memory, profile }) {
if (memory.has("replied")) return "skipped";
await page.goto(profile.messageUrl);
await page.type('div[role="textbox"]', `Quick bump on my earlier note, ${profile.firstName}. No rush—just wanted to keep it on your radar.`);
await page.keyboard.press('Enter');
}
The memory API is Redis-backed by default and persists across restarts. A reply from the human sets memory.set("replied", true) (captured via a DOM mutation observer we register in the background script).
Full OpenClaw config for LinkedIn outreach
Below is the trimmed gateway YAML that glues the pieces:
agent:
name: prospect-spider
description: "Browser-driven LinkedIn outreach with ethical guardrails"
tools:
- puppeteer
- github:openclaw/tools-helpers@v1.2.0 # text similarity, throttling
env:
LINKEDIN_EMAIL: "$vault.linkedin.email"
LINKEDIN_PASS: "$vault.linkedin.pass"
schedules:
- ./schedules/followUp.claw
limits:
dailyConnectionRequests: 80
dailyMessages: 120
maxRetries: 2
logging:
level: info
sinks:
- type: file
path: ~/.openclaw/logs/outreach.log
I call openclaw up --production on a $6/month Hetzner ARM box. Local surfacing is simpler but remote keeps my desktop free. Use a residential proxy only if your home IP changes aggressively—data center IPs shorten lifespans.
Handling LinkedIn ToS, legal, and ethical minefields
Short version: LinkedIn bans all scraping and automation. They’ve sued HiQ, PhantomBuster, and Octopus. Will they sue your side-project? Unknown, but risk tolerance varies.
- Personal vs client accounts – Running automation on a client’s account crosses from gray to dark gray because you’re a “service provider”.
- Data protection – If you store scraped data on EU residents, you’re under GDPR. Encrypt disks, purge on request.
- Honesty – Don’t pretend to have read a paper you didn’t. My profiles are hyper-short to avoid lies.
Community anecdote: @maria-salesforce on GitHub issues reported that pausing campaigns two days per week dropped restriction warnings to zero. LinkedIn seems to flag 7-day cadence.
Observability: knowing when things break before LinkedIn notices
Three alerts keep me sane:
- Login fail – Triggers after two consecutive 2FA prompts. Pauses all schedules.
- Restriction banner – DOM selector
div.artdeco-global-alertcontaining “We’ve restricted your account”. Sends webhook to Slack. - Response spike – If replies/hour > 20, agent switches to read-only so I can jump in manually.
OpenClaw’s observe helper looks like:
observe('page', async page => {
if (await page.$('div.artdeco-global-alert')) {
await pauseSchedules();
await notify("LinkedIn restriction banner detected, schedules paused");
}
});
Extending beyond LinkedIn: email fallback and CRM sync
Response rate plateau? Bolt on Composio’s Gmail tool:
openclaw add-tool @composio/gmail
Then route non-responders after 14 days into an email sequence. A tiny snippet:
if (!memory.has("replied") && memory.daysSince("connected") > 14) {
await tools.gmail.send({
to: profile.workEmail,
subject: `Quick follow-up, ${profile.firstName}`,
body: myLongerPitch,
});
}
For HubSpot logging, I use the REST helper bundled with OpenClaw 4.7.0:
await tools.rest.post('https://api.hubapi.com/crm/v3/objects/contacts', {
headers: { Authorization: `Bearer ${env.HUBSPOT_TOKEN}` },
body: { properties: { email: profile.workEmail, linkedin_url: profile.url } }
});
Practical next step
Spin a disposable LinkedIn alt, cap daily requests at 20, and let the bot run for a week. Watch logs, tweak sleeps, and—most importantly—review every outgoing message before scaling. Automation is leverage; aim it carefully.