Skip to content
MUNEEB SHAFIQ
WORKFLOW AUTOMATIONS04

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.
Private repository

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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

outreach-autopilot · flow
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
ComponentRole
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 normalizersUnwraps 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.
WaitPaces delivery between assembly and the Gmail send.
Gmail sendDelivers the message to the recruiter address with the resume attached and n8n attribution suppressed.
Google Sheets appendWrites 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.