This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "04a Outreach: Discover Contact + Draft Email",
"nodes": [
{
"parameters": {},
"id": "trg-manual",
"name": "Manual / After Scoring",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
180,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT a.id AS app_id, a.resume_id, j.id AS job_id, j.title, j.company_name, j.url, coalesce(j.description,'') AS description, left(res.raw_text, 1500) AS resume_snippet FROM app.applications a JOIN app.jobs j ON j.id = a.job_id JOIN app.resumes res ON res.id = a.resume_id WHERE a.status = 'ready' AND NOT EXISTS (SELECT 1 FROM app.outreach o WHERE o.application_id = a.id) ORDER BY a.created_at ASC LIMIT 5;",
"options": {}
},
"id": "pg-ready",
"name": "Get Ready Applications",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
380,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "=http://{{ $env.PLAYWRIGHT_HOST || 'playwright-service' }}:{{ $env.PLAYWRIGHT_PORT || '4000' }}/render",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-Api-Key",
"value": "={{ $env.PLAYWRIGHT_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ url: $json.url }) }}",
"options": {
"timeout": 60000,
"response": {
"response": {
"neverError": true
}
}
}
},
"id": "http-render",
"name": "Render Posting Page",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
600,
300
]
},
{
"parameters": {
"method": "POST",
"url": "=http://{{ $env.OLLAMA_HOST || 'ollama' }}:{{ $env.OLLAMA_PORT || '11434' }}/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $env.OLLAMA_MODEL_FAST || 'llama3.2:3b', stream: false, format: 'json', options: { temperature: 0 }, system: 'Extract a publicly listed recruiting/hiring contact email from the page text. Respond ONLY with JSON {\"email\": <string|null>, \"name\": <string|null>, \"title\": <string|null>}. Never guess or fabricate an address; if none is explicitly present, return null.', prompt: (($json.text || '').slice(0, 6000)) }) }}",
"options": {
"timeout": 60000
}
},
"id": "http-extract",
"name": "Extract Contact",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
820,
300
]
},
{
"parameters": {
"method": "POST",
"url": "=http://{{ $env.OLLAMA_HOST || 'ollama' }}:{{ $env.OLLAMA_PORT || '11434' }}/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: $env.OLLAMA_MODEL_REASONING || 'llama3.1:8b', stream: false, format: 'json', options: { temperature: 0.4 }, system: 'Write a short, respectful, personalized outreach email (<160 words) to a recruiter about a specific role. Truthful and specific to the resume highlights. No spammy or exaggerated language. Return ONLY JSON {\"subject\": \"...\", \"body\": \"...\"}.', prompt: 'ROLE: ' + $('Get Ready Applications').item.json.title + ' at ' + $('Get Ready Applications').item.json.company_name + '\\nJOB DESCRIPTION:\\n' + $('Get Ready Applications').item.json.description + '\\n\\nMY RESUME HIGHLIGHTS:\\n' + $('Get Ready Applications').item.json.resume_snippet }) }}",
"options": {
"timeout": 120000
}
},
"id": "http-outreach",
"name": "Generate Outreach",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1040,
300
]
},
{
"parameters": {
"jsCode": "// Build one CTE that creates the recruiter, the pending outreach, and the\n// approval token atomically, returning the token for the notification email.\nfunction q(s){ return String(s==null?'':s).replace(/'/g, \"''\"); }\nconst app = $('Get Ready Applications').item.json;\nlet contact = {}; try { contact = JSON.parse($('Extract Contact').item.json.response || '{}'); } catch(e) {}\nlet mail = {}; const mr = $('Generate Outreach').item.json.response || '';\ntry { mail = JSON.parse(mr); } catch(e) { const m = mr.match(/\\{[\\s\\S]*\\}/); if (m) { try { mail = JSON.parse(m[0]); } catch(_){} } }\nconst email = (contact && contact.email && /.+@.+\\..+/.test(contact.email)) ? contact.email : null;\nconst subject = mail.subject || ('Interest in the ' + app.title + ' role');\nconst body = mail.body || '';\nconst conf = email ? 0.6 : 0.0;\nconst recVals = `(${contact && contact.name ? `'${q(contact.name)}'` : 'NULL'}, ${email ? `'${q(email)}'` : 'NULL'}, '${q(app.company_name)}', ${contact && contact.title ? `'${q(contact.title)}'` : 'NULL'}, 'public-page', ${conf})`;\nconst query = `WITH rec AS (INSERT INTO app.recruiters (full_name,email,company_name,title,source,confidence) VALUES ${recVals} RETURNING id), o AS (INSERT INTO app.outreach (application_id, recruiter_id, subject, body, status) SELECT '${app.app_id}', rec.id, '${q(subject)}', '${q(body)}', 'pending_approval' FROM rec RETURNING id), ap AS (INSERT INTO app.approvals (entity_type, entity_id, action, payload) SELECT 'outreach', o.id, 'send_email', '{}'::jsonb FROM o RETURNING entity_id, token) SELECT ap.token AS token, ap.entity_id AS outreach_id FROM ap;`;\nreturn [{ json: { query, subject, body, recruiter_email: email } }];"
},
"id": "code-buildsql",
"name": "Build Outreach SQL",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1260,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ $json.query }}",
"options": {}
},
"id": "pg-persist",
"name": "Persist Outreach",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1480,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"fromEmail": "={{ $env.GMAIL_ADDRESS }}",
"toEmail": "={{ $env.GMAIL_ADDRESS }}",
"subject": "=[Approve?] Outreach: {{ $('Build Outreach SQL').item.json.subject }}",
"emailFormat": "text",
"text": "=A draft outreach email is waiting for your approval.\n\nTo (recruiter): {{ $('Build Outreach SQL').item.json.recruiter_email || 'NOT FOUND \u2014 add address in DB before approving' }}\nSubject: {{ $('Build Outreach SQL').item.json.subject }}\n\n---\n{{ $('Build Outreach SQL').item.json.body }}\n---\n\nAPPROVE: {{ $env.WEBHOOK_URL }}webhook/outreach-decision?token={{ $json.token }}&decision=approved\nREJECT: {{ $env.WEBHOOK_URL }}webhook/outreach-decision?token={{ $json.token }}&decision=rejected",
"options": {}
},
"id": "smtp-notify",
"name": "Email Me For Approval",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1700,
300
],
"credentials": {
"smtp": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Manual / After Scoring": {
"main": [
[
{
"node": "Get Ready Applications",
"type": "main",
"index": 0
}
]
]
},
"Get Ready Applications": {
"main": [
[
{
"node": "Render Posting Page",
"type": "main",
"index": 0
}
]
]
},
"Render Posting Page": {
"main": [
[
{
"node": "Extract Contact",
"type": "main",
"index": 0
}
]
]
},
"Extract Contact": {
"main": [
[
{
"node": "Generate Outreach",
"type": "main",
"index": 0
}
]
]
},
"Generate Outreach": {
"main": [
[
{
"node": "Build Outreach SQL",
"type": "main",
"index": 0
}
]
]
},
"Build Outreach SQL": {
"main": [
[
{
"node": "Persist Outreach",
"type": "main",
"index": 0
}
]
]
},
"Persist Outreach": {
"main": [
[
{
"node": "Email Me For Approval",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
postgressmtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
04a Outreach: Discover Contact + Draft Email. Uses postgres, httpRequest, emailSend. Event-driven trigger; 8 nodes.
Source: https://github.com/AskariJafri/job-search-os/blob/main/n8n/workflows/04-outreach/recruiter-and-outreach.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
06b Interview Prep Packages. Uses postgres, httpRequest, emailSend. Event-driven trigger; 6 nodes.
aula-00-mapa-do-n8n. Uses emailReadImap, stopAndError, httpRequest, graphql. Event-driven trigger; 46 nodes.
Reagendamiento_v2. Uses executeWorkflowTrigger, redis, httpRequest, n8n-nodes-evolution-api. Event-driven trigger; 89 nodes.
This workflow acts as a junior finance research analyst for a UK boutique M&A or corporate finance team. It listens for Slack messages, classifies the request, gathers company or market data, and prod
Disparador 1.8. Uses itemLists, postgres, emailSend, httpRequest. Scheduled trigger; 85 nodes.