Outreach Autopilot
Agentic n8n pipeline that finds jobs, locates recruiters, and sends personalized outreach automatically.
- n8n
- LangChain Agent nodes
- OpenAI GPT-4o
- Snov.io API
- Status
- POC. Exported n8n workflow with "active": false, so it runs on the manual "Execute Workflow" trigger rather than a schedule. Uploaded 2026-02-03 in two commits (Initial commit, then "Add files via upload"); no commits since. Repository is private.
By the numbers · 8
24
orchestrated workflow nodes
26
directed connections in the graph
4
external APIs integrated
2
independently prompted LLM agents
7
fields in the unified job schema
3
distinct merge strategies in one graph
10
postings pulled per source per run
224,627 bytes
size of the workflow definition
Summary
Outreach Autopilot is a 24-node n8n workflow that converts two job-board APIs into sent, personalized recruiter emails with no manual step in between. Postings from JSearch and a LinkedIn jobs API are normalized onto a shared seven-field schema, deduplicated against a Google Sheets ledger of prior outreach, screened by a fit filter, then enriched with recruiter contacts through Snov.io's OAuth API. Two separately-prompted GPT-4o agents draft the subject line and HTML body from the job description and a structured resume; the assembled message is sent through Gmail with the resume PDF attached and written back to the same sheet that feeds the next run's dedupe.
The problem
Finding relevant job openings is easy; reaching the right human at each company is the bottleneck. Every application requires locating a posting, identifying who actually hires for it, tracking down a working email address, writing something specific enough to earn a reply, attaching the right documents, and remembering which companies have already been contacted. Done by hand, this caps outreach at a handful of applications per day and pushes people toward generic templates that get ignored. The same bottleneck applies to any high-volume, personalized outbound motion, not just job search.
Approach
Parallel ingestion: a single trigger fans out to three sources at once - the JSearch endpoint on RapidAPI, a LinkedIn job-search endpoint on RapidAPI, and a Google Sheets read of companies already contacted.
Schema normalization: two Set nodes map each provider's distinct field names onto one common contract of job_id, job_title, employer_name, employer_website, job_description, apply_link, and source, so everything downstream reads a single shape.
Dedupe expressed as a join: a Merge node in keepNonMatches mode matches incoming job_id against the sheet's Job ID column and emits only unseen postings, making repeat runs idempotent without any extra state store.
Fit filter: an IF node screens job descriptions for analyst tooling keywords and screens out titles containing Senior, Lead, or Executive before any paid enrichment call is spent on the lead.
Contact enrichment: a two-step Snov.io call - a client_credentials OAuth request to mint an access token, then a domain-emails lookup scoped to Recruiter, Talent Acquisition, and Human Resources positions, with the employer domain stripped out of the website URL by an inline regex.
Split-agent generation: separate GPT-4o agents write the subject and the body under different system prompts. The subject agent is constrained to under 60 characters with an explicit spam-word blocklist; the body agent is constrained to 120-180 words of HTML with a no-fabrication instruction.
Assembly and pacing: a three-input Merge combines the subject, the body, and the resume PDF fetched as a binary file, and a Wait node sits between assembly and delivery so items dispatch on an interval rather than in one burst.
Closed loop: each sent message appends a row back to the same Google Sheet that is read at the start of the next run, so the send log and the dedupe source are the same artifact.
Architecture
Manual Trigger[JSearch API | LinkedIn Jobs API | Google Sheets read]Split Out + per-source Set normalizersMerge (union)Merge (keepNonMatches dedupe against sheet)IF fit filterSnov.io OAuth tokenSnov.io domain-emails lookupSet (agent payload)[Subject agent | Body agent | Resume PDF fetch]Merge (combine by position)WaitGmail send with attachmentGoogle Sheets append
| Component | Role |
|---|---|
| JSearch ingestion (HTTP Request) | Queries the RapidAPI JSearch endpoint for remote analyst roles posted in the last week, returning a nested data array of postings. |
| LinkedIn jobs ingestion (HTTP Request) | Queries a RapidAPI LinkedIn job-search endpoint filtered to remote-only roles across US, UAE, UK, and Europe, limit 10. |
| Split Out + dual Set normalizers | Unwraps the JSearch data array and maps both providers' fields onto one seven-key schema, stamping a source label on each record. |
| Merge (union) | Concatenates the two normalized streams into a single candidate pool. |
| Merge (keepNonMatches dedupe) | Anti-joins the candidate pool against the Google Sheet's Job ID column so only postings never contacted before proceed. |
| Quality Lead (IF) | Keyword-screens the job description for analyst tooling and screens out senior, lead, and executive titles before paid enrichment runs. |
| Snov Auth + Get Snov (HTTP Request pair) | Mints a client_credentials OAuth token, then looks up personal emails on the employer domain restricted to recruiter and HR positions. |
| Format for AI Agent (Set) | Reduces the enrichment response to the four fields the agents consume: recruiter name, recruiter email, job description, and company. |
| Subject agent + Body agent (LangChain agents on GPT-4o) | Two separately-prompted agents generate the subject line and the HTML email body, each backed by its own chatgpt-4o-latest model node. |
| Resume fetch (HTTP Request) | Pulls the resume PDF from the personal domain as a binary file for attachment. |
| Merge (combine by position) | Zips the subject, body, and resume binary into one item ready to send. |
| Wait | Paces delivery between assembly and the Gmail send. |
| Gmail send | Delivers the message to the recruiter address with the resume attached and n8n attribution suppressed. |
| Google Sheets append | Writes the contacted lead back to the ledger, closing the loop for the next run's dedupe. |
Trade-offs
Chose
Two independent LLM agents for subject and body
Over
One agent returning both fields in a single structured response
the two outputs carry incompatible constraints - the subject is capped under 60 characters and screened against a spam-word blocklist with no markup, while the body is 120-180 words of styled HTML - so each gets its own system prompt and its own model node rather than one prompt trying to satisfy both.
Chose
Google Sheets as both the dedupe ledger and the outreach log
Over
A database or dedicated CRM
the sheet appended to after every send is the same sheet read at the start of the next run, which delivers idempotency and a human-inspectable audit trail with zero infrastructure to operate.
Chose
Normalizing both providers to a shared field contract before merging
Over
Carrying provider-specific payload shapes downstream
the dedupe, filter, enrichment, prompt-assembly, and sheet-append nodes all read one schema, so onboarding a third job source requires adding one Set node rather than editing every node after the merge.
Chose
Snov.io domain search scoped to explicit recruiter job titles
Over
The Hunter.io domain-search node it replaced
Snov.io is queried with positions of Recruiter, Talent Acquisition, and Human Resources rather than a coarse department flag; the Hunter.io node is still present in the graph with its output disconnected, showing the swap.
Chose
A Wait node between message assembly and the Gmail send
Over
Dispatching every assembled item as fast as the branch produces it
the send step runs against a personal Gmail account, and interleaving a wait spaces delivery instead of emitting the whole batch in one burst.
At scale
Single-file repository: Workflow Workflow.json at 224,627 bytes alongside a 20-byte README containing only the repo name.
24 nodes and 26 directed connections spanning six node families: httpRequest, set, merge, googleSheets, gmail, and LangChain agent.
Three separate Merge nodes doing three different jobs in one graph - union of the two job sources, keepNonMatches anti-join for dedupe, and a three-input combineByPosition assembly of subject, body, and PDF attachment.
Two pinned sample datasets are saved inside the workflow for offline development, each holding a JSearch response of 10 job postings with 30 fields per posting.
The body agent's system prompt embeds a full structured resume covering 9 projects, 4 experience entries, 25 skills, and 10 certificates, which is what makes the generated emails specific rather than templated.
Enrichment is scoped to three recruiter-side job titles (Recruiter, Talent Acquisition, Human Resources) with a 10-result cap per employer domain.
My role
Sole author. Both commits in the repository are by Muneeb Shafiq. Designed the workflow graph end to end, wrote every node configuration and n8n expression (including the regex domain extraction and the cross-node item references), authored both agent system prompts and their constraint sets, and wired the Google Sheets feedback loop that makes repeat runs idempotent.
