Install, configure, and run both channels — LinkedIn discovery and email send — gated by flags you control end to end.
Install
A real, ordered sequence — each step depends on the one before it. Windows/PowerShell, run from the project root.
python -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install -U pip pip install -r requirements\local.txt python -m playwright install chromium python manage.py migrate
The LinkedIn browser driver installs straight from GitHub, not PyPI — the import name stays the same either way, so nothing downstream needs to know which source it came from.
LLM routing
One model drives search-keyword generation, lead qualification, and email drafting — set once, editable in Admin or at first-run onboarding.
Point it at a self-hosted Ollama model to run qualification for free, on your own hardware — a LAN host is tried first, a tunnelled remote host second, so the daemon keeps working off-network. An optional second, paid model only engages as a fallback: when the primary is genuinely unreachable, not when it just answers badly.
| Field | Where | Purpose |
|---|---|---|
| ai_model | Site config | Primary model, e.g. ollama:qwen2.5:latest |
| llm_api_key | Site config | Primary provider key (Ollama accepts any non-empty string) |
| llm_fallback_model | Site config | Second model, e.g. openai:gpt-4o. Blank = no fallback. |
| OLLAMA_BASE_URLS | core config | Ordered list of Ollama hosts — LAN, then tunnel |
Two channels, one funnel
A qualified lead forks onto exactly one of two channels. Each has its own setup, its own credentials, and its own risk profile.
Discovery channel
Login, People-search, and profile scraping run through a stealthed browser session against a real account — this automates a real LinkedIn account and is against its User Agreement; read Legal & risk before enabling it.
| Field | Default | Meaning |
|---|---|---|
| username / password | — | Real LinkedIn login; session cookies are saved after the first login |
| legal_accepted | false | Must be true before the browser session runs |
| connect_daily_limit | 20 | Max connection requests/day — inert unless connect is enabled |
| follow_up_daily_limit | 25 | Max LinkedIn follow-ups/day — inert unless connect is enabled |
Send channel
Plain SMTP, from a mailbox the operator owns — no third-party sending platform in between. A lead only forks here once a work email resolves and a mailbox exists to send from.
| Field | Default | Meaning |
|---|---|---|
| host / port | smtp.gmail.com : 587 | SMTP send |
| imap_host / port | imap.gmail.com : 993 | Reply-read, for the follow-up agent |
| username / password | — | Google account + app password (2-Step Verification required) |
| daily_limit | 30 | Warm-safe sends/day, enforced per mailbox |
Coordination — LinkedIn feeds Email
One qualification funnel, two entry points, both landing in the same place. A lead that resolves a work email forks straight to email. A lead that doesn't sits at the LinkedIn boundary instead — and goes no further while connect stays off.
Two complete setups, depending on how much of the pipeline should be live:
Email-fed leads only (LinkedIn stays find-only) — arm the campaign, confirm a mailbox exists, leave connect off. Every lead that doesn't resolve an email simply stops at the LinkedIn boundary; that's the entire effect of leaving connect off.
Both entry points live — everything above, plus flipping the global LinkedIn-connect switch on. That's a real decision on real ToS exposure (see Legal & risk), not reduced by anything else on this page, and it re-arms connect for every armed campaign at once, not just the one intended.
Campaigns
A campaign is the unit everything else hangs off — what's being offered, who to target, and which channels it's allowed to use.
| Field | Type | Meaning |
|---|---|---|
| product_docs | text | What's being offered — fed into every agent prompt |
| campaign_objective | text | Who to target and why — drives search, qualification, drafting |
| booking_link | URL | Included in follow-ups when suggesting a meeting |
| users | link | Which operator account(s) run it — unlinked = fully inert |
Safety flags
Four switches, two scopes. A global switch keeps LinkedIn connect off by policy; a per-campaign switch is the real activate/deactivate control — stage a campaign fully, arm it only when ready.
False by default, meant to stay that way — search, scrape, and qualify still run; a connection request is simply never sent.
The activate switch. Off (default) — the campaign is fully dormant: no discovery, no email, no follow-up, checked before anything else.
On by default — a first send waits for a human to clear the draft. Never gates a follow-up reply once a thread exists.
Blocks LinkedIn discovery/connect for one campaign only, independent of every other flag — for leads sourced outside LinkedIn entirely.
It re-arms connect for every armed, non-email_only campaign at once — not just the one intended. Check each campaign's flags first.
Running it
One process, one loop.
python manage.py rundaemon
On startup: migrate → onboard, if unconfigured → validate → the task-queue loop. The loop plans discovery/qualify/connect/email slots per active campaign and works through them at a human-paced rhythm — bursts of 45–65 minutes, breaks of 10–20 — reconciling on every idle cycle. Everything the daemon is doing is browsable and editable from the admin console.
Legal & risk — read before enabling either channel
The project's own legal notice is the authoritative version; this is a pointer, not a substitute.
Bots, scripts, and automated browsing/messaging/connecting are explicitly prohibited. LinkedIn actively detects and penalizes this — there's no automation volume low enough to be safe, and consequences run up to permanent account suspension. That's true regardless of the connect flag; the flag controls what this tool does, not what LinkedIn allows.
Email-finding and sending run through third-party services and infrastructure the operator configures and owns — whoever sets it up is the data controller for any address resolved and stored, and is solely responsible for anti-spam compliance (CAN-SPAM / GDPR / CASL, as applicable) for anything sent.