This workflow follows the Chainllm → Emailsend 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": "Job-Posting-agent",
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"documentId": {
"__rl": true,
"value": "YOUR_GOOGLE_SHEETS_SPREADSHEET_ID",
"mode": "list",
"cachedResultName": "Job Postings",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEETS_SPREADSHEET_ID/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "AI Job Postings",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEETS_SPREADSHEET_ID/edit#gid=0"
},
"event": "rowAdded",
"options": {}
},
"type": "n8n-nodes-base.googleSheetsTrigger",
"typeVersion": 1,
"position": [
-944,
112
],
"id": "80ca69db-d6a8-421c-86f4-9260f7505dfe",
"name": "Google Sheets Trigger",
"credentials": {
"googleSheetsTriggerOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// ============================================================\n// VALIDATE INPUT & PREPARE FOR AI PROCESSING\n// ============================================================\n\n// Extract data from trigger ( use $json instead of $input.all())\nconst inputData = $json;\n\n// Check if Raw_Data exists\nif (!inputData.Raw_Data || inputData.Raw_Data.trim() === '') {\n throw new Error('Raw_Data column is empty. Please paste job details.');\n}\n\n// Clean raw data (remove excessive whitespace, normalize)\nconst cleanedRawData = inputData.Raw_Data\n .trim()\n .replace(/\\n{3,}/g, '\\n\\n') // Max 2 consecutive line breaks\n .replace(/\\s{2,}/g, ' '); // Max 1 space between words\n\n// Generate unique job ID\nconst jobId = `JOB${Date.now()}`;\n\n// Prepare output for AI node\nreturn {\n json: {\n job_id: jobId,\n raw_data: cleanedRawData,\n timestamp: new Date().toISOString(),\n original_row: inputData\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-720,
112
],
"id": "0dc438b6-986c-4cf4-980a-fe3ebe5df6da",
"name": "Validate Input Node"
},
{
"parameters": {
"promptType": "define",
"text": "==Extract job details and create an engaging WhatsApp post from this raw data:\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n{{ $('Validate Input Node').item.json.raw_data }}\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nIMPORTANT INSTRUCTIONS:\n1. If this contains multiple processes from the SAME company (like \"Concentrix INT: JPMC, Optus, VMO2\"), combine them into ONE post with all processes listed\n2. If information is missing, use intelligent defaults:\n - Location: Check for city names mentioned anywhere\n - Salary: \"Competitive\" or \"As per industry standards\"\n - Experience: \"Freshers welcome\" if not mentioned\n3. Detect urgency keywords: urgent, immediate, limited, deadline, hiring now, etc.\n4. Add 3-5 relevant hashtags based on company, location, role\n5. Include a clear call-to-action (Apply now, Contact us, etc.)\n6. Use line breaks for mobile readability\n\nIf you detect this is a hiring update with MULTIPLE COMPANIES, focus on creating ONE comprehensive post listing all opportunities. Use the format:\n\n\ud83c\udfaf *MULTIPLE HIRING OPPORTUNITIES*\n\n*Company 1: [Name]*\n\u25aa\ufe0f Process 1\n\u25aa\ufe0f Process 2\n...\n\n*Company 2: [Name]*\n\u25aa\ufe0f Process 1\n...\n\nGenerate the post now:",
"messages": {
"messageValues": [
{
"message": "You are an expert job posting processor for a BPO recruitment agency in India. Your task is to: 1. Extract ALL job details from raw, unstructured text (even if messy/incomplete) 2. INTELLIGENTLY handle multiple processes: - If SAME company has multiple processes \u2192 Combine into ONE post listing all processes - If DIFFERENT companies \u2192 Create separate posts (but user will handle splitting) 3. Fill missing information with sensible defaults 4. Generate an engaging, professional WhatsApp job post with natural variety CRITICAL RULES: - When you see multiple processes under ONE company, list them as bullet points in a single post - Use emojis strategically (not excessive, professional tone) - Keep posts mobile-friendly (short lines, clear structure) - Add relevant hashtags based on location/role - Keep total post under 250 words - Use Indian currency format (\u20b9) OUTPUT FORMAT (STRICT JSON): { \"company_count\": 1, \"company_name\": \"Primary company name\", \"processes\": [\"Process 1\", \"Process 2\", ...], \"location\": \"City/Location\", \"salary_range\": \"\u20b9XX,XXX - \u20b9XX,XXX or 'Competitive' if not mentioned\", \"experience\": \"X years or 'Freshers' or 'Not specified'\", \"shift_details\": \"Day/Night/Rotational or 'Not specified'\", \"key_requirements\": \"Brief summary of requirements\", \"additional_info\": \"Benefits, perks, other details\", \"urgency\": \"high/normal\", \"formatted_post\": \"Complete WhatsApp-ready message here\" } For formatted_post, naturally vary between these styles: - Professional & structured (most common) - Urgent & action-oriented (if urgency detected) - Friendly & conversational (occasionally) - Use natural variation in emoji placement, CTAs, and phrasing"
},
{
"type": "HumanMessagePromptTemplate",
"message": "=Extract job details and create an engaging WhatsApp post from this raw data: \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 {{ $json.raw_data }} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 IMPORTANT INSTRUCTIONS: 1. If this contains multiple processes from the SAME company (like \"Concentrix INT: JPMC, Optus, VMO2\"), combine them into ONE post with all processes listed 2. If information is missing, use intelligent defaults: - Location: Check for city names mentioned anywhere - Salary: \"Competitive\" or \"As per industry standards\" - Experience: \"Freshers welcome\" if not mentioned 3. Detect urgency keywords: urgent, immediate, limited, deadline, hiring now, etc. 4. Add 3-5 relevant hashtags based on company, location, role 5. Include a clear call-to-action (Apply now, Contact us, etc.) 6. Use line breaks for mobile readability If you detect this is a hiring update with MULTIPLE COMPANIES, focus on creating ONE comprehensive post listing all opportunities. Use the format: \ud83c\udfaf *MULTIPLE HIRING OPPORTUNITIES* *Company 1: [Name]* \u25aa\ufe0f Process 1 \u25aa\ufe0f Process 2 ... *Company 2: [Name]* \u25aa\ufe0f Process 1 ... Generate the post now:"
}
]
},
"batching": {}
},
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1.9,
"position": [
-528,
112
],
"id": "fb4b8eb2-9985-4340-bf35-55f9598549ee",
"name": "Basic LLM Chain"
},
{
"parameters": {
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"typeVersion": 1,
"position": [
-528,
304
],
"id": "6ac73aa2-4ca6-4efb-95f4-7c1f1fd2b3b4",
"name": "Google Gemini Chat Model",
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// ============================================================\n// PARSE GEMINI AI RESPONSE\n// ============================================================\n\nconst aiResponse = $input.first().json;\n\n// Extract the AI's text response\nlet aiContent;\nif (aiResponse.content && Array.isArray(aiResponse.content)) {\n aiContent = aiResponse.content[0].text;\n} else if (typeof aiResponse.content === 'string') {\n aiContent = aiResponse.content;\n} else if (aiResponse.text) {\n aiContent = aiResponse.text;\n} else {\n throw new Error('Could not extract AI response. Check Gemini node output.');\n}\n\n// Parse JSON from AI response\n// Sometimes AI wraps JSON in markdown code blocks, handle that\nlet parsedData;\ntry {\n // Try to find JSON in markdown code blocks first\n const jsonMatch = aiContent.match(/```json\\s*([\\s\\S]*?)\\s*```/);\n if (jsonMatch) {\n parsedData = JSON.parse(jsonMatch[1]);\n } else {\n // Try parsing the entire content as JSON\n parsedData = JSON.parse(aiContent);\n }\n} catch (error) {\n // If parsing fails, create a fallback structure\n console.error('AI JSON parsing failed:', error);\n \n // Extract formatted_post even if JSON parsing failed\n const postMatch = aiContent.match(/\\\"formatted_post\\\":\\s*\\\"([\\s\\S]*?)\\\"/);\n const extractedPost = postMatch ? postMatch[1].replace(/\\\\n/g, '\\n') : aiContent;\n \n parsedData = {\n company_name: 'Unknown',\n processes: [],\n location: 'Not specified',\n salary_range: 'Competitive',\n experience: 'Not specified',\n formatted_post: extractedPost,\n parsing_note: 'Fallback used - AI response was not valid JSON'\n };\n}\n\n// Get original job_id from previous node (FIXED: correct node name)\nconst jobId = $('Validate Input Node').first().json.job_id;\n\n// Combine everything for next nodes\nreturn {\n json: {\n job_id: jobId,\n company_name: parsedData.company_name || 'Unknown',\n processes: parsedData.processes || [],\n location: parsedData.location || 'Not specified',\n salary_range: parsedData.salary_range || 'Competitive',\n experience: parsedData.experience || 'Not specified',\n shift_details: parsedData.shift_details || 'Not specified',\n key_requirements: parsedData.key_requirements || '',\n additional_info: parsedData.additional_info || '',\n urgency: parsedData.urgency || 'normal',\n formatted_message: parsedData.formatted_post,\n post_date: new Date().toLocaleDateString('en-IN'),\n post_time: new Date().toLocaleTimeString('en-IN'),\n extracted_data: JSON.stringify(parsedData, null, 2),\n parsing_status: parsedData.parsing_note || 'success'\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-192,
112
],
"id": "793096aa-63d0-422c-ab47-6fa818573c8a",
"name": "Parse AI Response"
},
{
"parameters": {
"method": "POST",
"url": "https://gate.whapi.cloud/messages/text",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "to",
"value": "YOUR_WHATSAPP_GROUP_ID_1@g.us"
},
{
"name": "body",
"value": "={{ $('Parse AI Response').item.json.formatted_message }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
48,
0
],
"id": "54457270-de48-4d26-a396-429091d60df2",
"name": "Send to Group 1",
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"amount": 2
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
272,
0
],
"id": "fb8046ea-6347-41c4-b183-3088f11f8fe2",
"name": "Wait 2s (1)"
},
{
"parameters": {
"method": "POST",
"url": "https://gate.whapi.cloud/messages/text",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "to",
"value": "YOUR_WHATSAPP_GROUP_ID_2@g.us"
},
{
"name": "body",
"value": "={{ $('Parse AI Response').item.json.formatted_message }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
496,
0
],
"id": "a154629e-68dd-43b1-a35a-cd077d7389ce",
"name": "Send to Group 2",
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"amount": 2
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
720,
0
],
"id": "721fd1e8-da0d-49f7-a96a-164cf326d2e0",
"name": "Wait 2s (2)"
},
{
"parameters": {
"method": "POST",
"url": "https://gate.whapi.cloud/messages/text",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "to",
"value": "YOUR_WHATSAPP_GROUP_ID_3@g.us"
},
{
"name": "body",
"value": "={{ $('Parse AI Response').item.json.formatted_message }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
928,
0
],
"id": "7a28f704-a621-4cf3-8f2d-6464993a671a",
"name": "Send to Group 3",
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "YOUR_GOOGLE_SHEETS_SPREADSHEET_ID",
"mode": "list",
"cachedResultName": "Job Postings",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEETS_SPREADSHEET_ID/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "YOUR_POSTING_LOG_SHEET_GID",
"mode": "list",
"cachedResultName": "Posting Log",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEETS_SPREADSHEET_ID/edit#gid=YOUR_POSTING_LOG_SHEET_GID"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Job_ID": "={{ $('Parse AI Response').item.json.job_id }}",
"Company": "={{ $('Parse AI Response').item.json.company_name }}",
"Processes": "={{ $('Parse AI Response').item.json.processes.join(', ') }}",
"Posted_DateTime": "={{ $now.toISO() }}",
"Groups_Sent": "Group1, Group2, Group3",
"Message_Content": "={{ $('Parse AI Response').item.json.formatted_message }}",
"Extracted_Data": "={{ $('Parse AI Response').item.json.extracted_data }}",
"Status": "SUCCESS"
},
"matchingColumns": [],
"schema": [
{
"id": "Job_ID",
"displayName": "Job_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Company",
"displayName": "Company",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Processes",
"displayName": "Processes",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Posted_DateTime",
"displayName": "Posted_DateTime",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Groups_Sent",
"displayName": "Groups_Sent",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Message_Content",
"displayName": "Message_Content",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Extracted_Data",
"displayName": "Extracted_Data",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Status",
"displayName": "Status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1152,
112
],
"id": "b925001a-5a38-4bb9-88d3-9336266885bd",
"name": "Append row in sheet",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "YOUR_GOOGLE_SHEETS_SPREADSHEET_ID",
"mode": "list",
"cachedResultName": "Job Postings"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "AI Job Postings"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Job_ID": "={{ $('Parse AI Response').first().json.job_id }}",
"Posted_Status": "SUCCESS",
"Posted_Date": "={{ $now.toFormat('dd MMM yyyy HH:mm') }}",
"row_number": 0
},
"matchingColumns": [
"Job_ID"
],
"schema": [
{
"id": "Raw_Data",
"displayName": "Raw_Data",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "Job_ID",
"displayName": "Job_ID",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Posted_Status",
"displayName": "Posted_Status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Posted_Date",
"displayName": "Posted_Date",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Notes",
"displayName": "Notes",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1376,
112
],
"id": "2bd85e5f-3d30-4ca7-ae56-957d13ccaeb1",
"name": "Update row in sheet",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.errorTrigger",
"typeVersion": 1,
"position": [
-400,
496
],
"id": "f48a9d02-d31c-45d4-bcdb-3992b9408cba",
"name": "Error Trigger"
},
{
"parameters": {
"fromEmail": "your-email@example.com",
"toEmail": "your-email@example.com",
"subject": "=\u26a0\ufe0f Job Posting Failed - {{ $now.toFormat('dd MMM yyyy HH:mm') }}",
"html": "=<h3>Job posting workflow failed.</h3>\n<b>Job ID:</b> {{ $json.job_id }}<br>\n<b>Error:</b> {{ $json.error.message }}<br>\n<br>\n<b>Raw Data Preview:</b><br>\n<pre>{{ $json.raw_data?.substring(0, 200) }}...</pre>\n<br>\nPlease check n8n dashboard for details.",
"options": {}
},
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
-176,
496
],
"id": "7e4450cf-32f6-4953-a0cd-be2ef9411cb7",
"name": "Send Error Email",
"credentials": {
"smtp": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Google Sheets Trigger": {
"main": [
[
{
"node": "Validate Input Node",
"type": "main",
"index": 0
}
]
]
},
"Validate Input Node": {
"main": [
[
{
"node": "Basic LLM Chain",
"type": "main",
"index": 0
}
]
]
},
"Basic LLM Chain": {
"main": [
[
{
"node": "Parse AI Response",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "Basic LLM Chain",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Parse AI Response": {
"main": [
[
{
"node": "Send to Group 1",
"type": "main",
"index": 0
}
]
]
},
"Send to Group 1": {
"main": [
[
{
"node": "Wait 2s (1)",
"type": "main",
"index": 0
}
]
]
},
"Wait 2s (1)": {
"main": [
[
{
"node": "Send to Group 2",
"type": "main",
"index": 0
}
]
]
},
"Send to Group 2": {
"main": [
[
{
"node": "Wait 2s (2)",
"type": "main",
"index": 0
}
]
]
},
"Wait 2s (2)": {
"main": [
[
{
"node": "Send to Group 3",
"type": "main",
"index": 0
}
]
]
},
"Send to Group 3": {
"main": [
[
{
"node": "Append row in sheet",
"type": "main",
"index": 0
}
]
]
},
"Append row in sheet": {
"main": [
[
{
"node": "Update row in sheet",
"type": "main",
"index": 0
}
]
]
},
"Error Trigger": {
"main": [
[
{
"node": "Send Error Email",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "",
"id": "",
"tags": []
}
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.
googlePalmApigoogleSheetsOAuth2ApigoogleSheetsTriggerOAuth2ApihttpHeaderAuthsmtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Job-Posting-agent. Uses googleSheetsTrigger, chainLlm, lmChatGoogleGemini, httpRequest. Event-driven trigger; 14 nodes.
Source: https://github.com/ArshhAnsari/n8n-whatsapp-posting-agent/blob/main/WA-Job-Posting-agent-template.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.
This workflow automates the entire process of creating SEO-optimized meta titles and descriptions. It analyzes your webpage, spies on top-ranking competitors for the same keywords, and then uses a mul
This workflow scrapes Google Maps via Decodo API, analyzes each business using Google Gemini 3 Flash, scores lead quality, and generates ready to send outreach emails. Time Savings: Reduces manual lea
> by Thomas Vie Thomas@pollup.net
This workflow automates your entire first-round resume screening process. When a candidate submits your Google Form application, the workflow triggers, extracts their details, scores them against the
Workflow 2823. Uses httpRequest, lmChatOpenAi, outputParserStructured, chainLlm. Event-driven trigger; 19 nodes.