A local-first LinkedIn outreach tool for uploading a CSV of contacts, previewing personalized messages, and sending them through a local Playwright-controlled LinkedIn browser session.
This project is intentionally designed as a local app with a browser UI, not a hosted web app. The app runs at localhost:3000, and the LinkedIn session stays on the user's machine inside a local Playwright browser profile.
LinkedIn automation depends on an authenticated browser session. Keeping the automation local avoids requiring users to share LinkedIn passwords, cookies, or session data with a hosted server.
A hosted version would likely require cloud browsers, credential handling, or a local bridge anyway. For this product, the safer and simpler architecture is:
- Local web UI at
http://localhost:3000 - Local Playwright browser profile for LinkedIn
- Local CSV upload and message queue
- Local safety ledger for caps, failures, and send history
- Node.js
- npm
- Google Chrome or Playwright Chromium
- A LinkedIn account you can log into locally
Clone the repo and install dependencies:
git clone git@github.com:bilguunbay/LinkedIn-Outreach.git
cd LinkedIn-Outreach
git checkout feat/content-script-send-flow
npm install
npx playwright install chromiumBefore using the web app to send messages, set up the local Playwright LinkedIn session once:
npm run draft:linkedin -- \
--url "https://www.linkedin.com/in/alison-nguyen-549772295/" \
--message "Login test"A Playwright browser window will open. Log into LinkedIn in that browser. After login is complete, the app will reuse the local session stored in .playwright-linkedin-profile/.
That folder is local-only and should not be committed.
Start the local server:
npm run devThen open:
http://localhost:3000
Upload a CSV with these columns:
Company,Founder / CEO,LinkedIn URL,Role
TheOperatingCompany,Joshua Schiller,https://www.linkedin.com/in/joshschiller/,FounderThe app also accepts common header variants like:
company_namefounder_ceonamelinkedinprofile_urlroletitle
- Start the app with
npm run dev. - Open
http://localhost:3000. - Upload a CSV.
- Edit the message template.
- Review the preview.
- Click
Send Messagefor a contact.
The app queues sends one at a time. It does not run multiple LinkedIn sends concurrently.
The UI includes safety settings for:
- Daily new-profile cap
- Active sending hours
- Min/max delay before clicking Send
- Repeat-profile override
- Duplicate-message override
By default, the app is conservative. To intentionally test the same profile more than once, enable:
Allow repeat sends to profiles already messaged by this tool
If the exact same message text was already sent recently, also enable:
Allow exact duplicate message text sent in the last 7 days
These overrides increase account risk and require acknowledgment in the UI.
To check whether Playwright itself is working outside the web UI:
npm run send:linkedin -- \
--url "https://www.linkedin.com/in/bilguunbayarkhuu/" \
--message "Hi Bilguun,
Hope you're doing well. I wanted to reach out and reconnect." \
--min-delay 1 \
--max-delay 3 \
--ignore-active-hours \
--allow-repeatUse this when you want to separate Playwright issues from web app issues.
npm run devRuns the local web app at http://localhost:3000.
npm run checkRuns JavaScript syntax checks for the server, web app, Playwright sender, and extension files.
npm run status:linkedinPrints the local safety ledger status.
npm run draft:linkedin -- --url "https://www.linkedin.com/in/example/" --message "Test"Opens a LinkedIn profile and drafts a message without clicking Send.
npm run send:linkedin -- --url "https://www.linkedin.com/in/example/" --message "Test"Sends through Playwright using the configured safety checks.
Check the Activity Log. Common causes:
- Outside active hours
- Daily cap reached
- Already sent to this profile
- Duplicate message text
- LinkedIn login/checkpoint required
The app intentionally waits before clicking Send. Check the Delay setting in the top safety bar. The default can be 60-180 seconds.
Run the one-time login command again:
npm run draft:linkedin -- \
--url "https://www.linkedin.com/in/alison-nguyen-549772295/" \
--message "Login test"Log in inside the Playwright browser, then retry from the web app.
If you already sent to a profile today, enable the repeat-profile override in Safety Settings or add --allow-repeat when using the terminal command.
When the local server is running, visit:
http://localhost:3000/api/jobs
This returns recent job status and logs.
LinkedIn restricts unauthorized automation. This tool includes conservative defaults, queueing, daily caps, active hours, and delays, but no automation can eliminate account risk.
Use small volumes, review messages carefully, and avoid behavior that looks repetitive or spammy.