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 →
{
"updatedAt": "2026-04-02T17:21:25.530Z",
"createdAt": "2026-04-02T17:20:27.976Z",
"id": "RjELbd8jHpE0mYLy",
"name": "WF4-GHOST: Ghosting & Follow-Up Engine",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8,14,20 * * *"
}
]
}
},
"id": "sched",
"name": "Every 6 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT a.id, a.company_name, a.job_title, a.pipeline_track, a.current_state,\n EXTRACT(EPOCH FROM (NOW() - a.state_changed_at)) / 86400 AS days_in_state,\n EXTRACT(EPOCH FROM (NOW() - a.applied_at)) / 86400 AS days_since_applied,\n a.follow_up_count,\n a.last_follow_up_at,\n a.follow_up_snoozed_until,\n a.applied_at,\n a.state_changed_at,\n a.reference_number\nFROM applications a\nWHERE tenant_id = '{{ $('Loop Over Tenants').item.json.tenant_id }}' AND a.is_active = true\n AND a.current_state IN ('applied', 'acknowledged', 'screening', 'interviewing', 'academic_longlisted', 'academic_shortlisted')\n AND (a.follow_up_snoozed_until IS NULL OR a.follow_up_snoozed_until < NOW())\nORDER BY a.state_changed_at ASC;",
"options": {}
},
"id": "pg_fetch",
"name": "Fetch Active Applications",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
650,
0
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const apps = $input.all();\nconst results = [];\n\nfor (const app of apps) {\n const a = app.json;\n const track = a.pipeline_track;\n const state = a.current_state;\n const daysInState = parseFloat(a.days_in_state) || 0;\n const followUps = a.follow_up_count || 0;\n\n const thresholds = {\n corporate: {\n applied: { warn: 7, ghost: 14, maxFollowups: 2 },\n acknowledged: { warn: 10, ghost: 21, maxFollowups: 1 },\n screening: { warn: 7, ghost: 14, maxFollowups: 1 },\n interviewing: { warn: 7, ghost: 14, maxFollowups: 1 },\n },\n academic: {\n applied: { warn: 21, ghost: 56, maxFollowups: 1 },\n acknowledged: { warn: 28, ghost: 56, maxFollowups: 1 },\n academic_longlisted: { warn: 28, ghost: 56, maxFollowups: 1 },\n academic_shortlisted: { warn: 21, ghost: 42, maxFollowups: 1 },\n screening: { warn: 14, ghost: 28, maxFollowups: 1 },\n interviewing: { warn: 21, ghost: 42, maxFollowups: 1 },\n }\n };\n\n const t = ((thresholds[track] || thresholds.corporate)[state]);\n if (!t) {\n results.push({ json: {\n tenant_id: tenantId, ...a, action: 'ok' } });\n continue;\n }\n\n let action = 'ok';\n if (daysInState >= t.ghost && followUps >= t.maxFollowups) {\n action = 'ghosted';\n } else if (daysInState >= t.warn && followUps < t.maxFollowups) {\n action = 'follow_up_due';\n }\n\n const reminderType = followUps === 0 ? 'initial_follow_up' : 'second_follow_up';\n\n results.push({ json: {\n tenant_id: tenantId, ...a, action, thresholdDays: t.warn, ghostDays: t.ghost, reminderType, daysInState: Math.round(daysInState) } });\n}\n\nreturn results;"
},
"id": "code_eval",
"name": "Evaluate Follow-Up Status",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
0
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "follow_up_due",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "follow_up_due"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "ghosted",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "ghosted"
}
]
},
"options": {
"allMatchingOutputs": false,
"fallbackOutput": "extra"
}
},
"id": "switch_action",
"name": "Switch Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
1150,
0
]
},
{
"parameters": {
"jsCode": "const a = $input.first().json;\nconst company = a.company_name;\nconst title = a.job_title;\nconst days = a.daysInState;\nconst track = a.pipeline_track;\nconst state = a.current_state;\nconst ref = a.reference_number ? ` (Ref: ${a.reference_number})` : '';\n\nlet suggestedAction = '';\nlet template = '';\n\nif (track === 'academic') {\n suggestedAction = `Check university HR portal for status update. Academic hiring timelines are longer \u2014 ${days} days is approaching the follow-up threshold.`;\n template = `Dear Hiring Committee,\\n\\nI submitted my application for the ${title} position${ref} and wanted to confirm it was received. I remain very interested in this opportunity and would welcome any update on the timeline.\\n\\nKind regards,\\nSelvi`;\n} else {\n suggestedAction = `Send a polite follow-up email to ${company}. You applied ${days} days ago with no response.`;\n template = `Dear Hiring Manager,\\n\\nI applied for the ${title} role${ref} at ${company} and wanted to follow up. I'm very interested in this opportunity and would appreciate any update on the status of my application.\\n\\nBest regards,\\nSelvi`;\n}\n\nconst subject = `Follow-up needed: ${title} at ${company} (${days} days)`;\nconst bodyHtml = `<h3>Follow-Up Reminder</h3><p><strong>${title}</strong> at <strong>${company}</strong></p><p>You applied ${days} days ago (${track} track, state: ${state}).</p><p><strong>Suggested action:</strong> ${suggestedAction}</p><h4>Suggested follow-up email:</h4><pre>${template}</pre>`;\n\nreturn [{ json: { ...a, subject, bodyHtml, suggestedAction, template } }];"
},
"id": "code_followup",
"name": "Generate Follow-Up Template",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1400,
-100
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO follow_up_log (tenant_id, application_id, reminder_type, suggested_action, follow_up_template, sent_at) VALUES ('{{ $('Loop Over Tenants').item.json.tenant_id }}', '{{ $json.id }}', '{{ $json.reminderType }}', '{{ $json.suggestedAction }}', '{{ $json.template }}', NOW());",
"options": {}
},
"id": "pg_followup_log",
"name": "Insert Follow-Up Log",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1650,
-100
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE applications SET\n follow_up_count = follow_up_count + 1,\n last_follow_up_at = NOW(),\n next_follow_up_at = NOW() + INTERVAL '7 days'\nWHERE tenant_id = '{{ $('Loop Over Tenants').item.json.tenant_id }}' AND id = '{{ $json.id }}';",
"options": {}
},
"id": "pg_update_followup",
"name": "Update Follow-Up Count",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1900,
-100
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notification_queue (tenant_id, application_id, notification_type, priority, subject, body_html, body_text) VALUES ('{{ $('Loop Over Tenants').item.json.tenant_id }}', '{{ $json.id }}',\n 'follow_up_reminder',\n 'high',\n '{{ $json.subject }}',\n '{{ $json.bodyHtml }}',\n '{{ $json.suggestedAction }}');",
"options": {}
},
"id": "pg_queue_followup",
"name": "Queue Follow-Up Notification",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2150,
-100
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT transition_application_state(\n '{{ $json.id }}'::uuid,\n 'ghosted',\n 'ghosting_detected',\n 'wf4_ghost',\n '{\"days_waited\": {{ $json.daysInState }}, \"follow_ups_sent\": {{ $json.follow_up_count }}}'::jsonb,\n 'Automatically detected as ghosted after {{ $json.daysInState }} days with no response'\n);",
"options": {}
},
"id": "pg_ghost_transition",
"name": "Transition to Ghosted",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1400,
100
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notification_queue (tenant_id, application_id, notification_type, priority, subject, body_html, body_text) VALUES ('{{ $('Loop Over Tenants').item.json.tenant_id }}', '{{ $json.id }}',\n 'ghosting_detected',\n 'medium',\n 'Ghosted: {{ $json.job_title }} at {{ $json.company_name }}',\n '<h3>Application Marked as Ghosted</h3><p><strong>{{ $json.job_title }}</strong> at <strong>{{ $json.company_name }}</strong></p><p>After {{ $json.daysInState }} days with no response and {{ $json.follow_up_count }} follow-up(s) sent, this application has been automatically marked as ghosted.</p><p>Pipeline track: {{ $json.pipeline_track }}</p>',\n 'Application to {{ $json.company_name }} for {{ $json.job_title }} marked as ghosted after {{ $json.daysInState }} days.'\n);",
"options": {}
},
"id": "pg_queue_ghost",
"name": "Queue Ghosting Notification",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1650,
100
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT a.id, a.company_name, a.job_title, a.current_state, j.expires_at,\n EXTRACT(EPOCH FROM (j.expires_at - NOW())) / 3600 AS hours_until_deadline\nFROM applications a\nJOIN jobs j ON a.job_id = j.id\nWHERE tenant_id = '{{ $('Loop Over Tenants').item.json.tenant_id }}' AND a.current_state IN ('discovered', 'shortlisted', 'cv_tailored')\n AND j.expires_at BETWEEN NOW() AND NOW() + INTERVAL '48 hours'\n AND j.expires_at > NOW();",
"options": {}
},
"id": "pg_deadlines",
"name": "Check Approaching Deadlines",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
650,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst tenantId = items[0]?.json?.tenant_id || '';\nconst results = [];\nfor (const item of items) {\n const a = item.json;\n if (!a.id) continue;\n const hours = Math.round(parseFloat(a.hours_until_deadline) || 0);\n results.push({ json: {\n tenant_id: tenantId,\n ...a,\n hours_remaining: hours,\n subject: `DEADLINE: ${a.job_title} at ${a.company_name} expires in ${hours} hours`,\n bodyHtml: `<h3>Application Deadline Approaching</h3><p><strong>${a.job_title}</strong> at <strong>${a.company_name}</strong></p><p>Deadline in <strong>${hours} hours</strong>.</p><p>Current status: ${a.current_state}. Apply now or mark as expired.</p>`\n }});\n}\nreturn results.length > 0 ? results : [{ json: { skip: true } }];"
},
"id": "code_deadline",
"name": "Format Deadline Alerts",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.skip }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notTrue"
}
}
],
"combinator": "and"
}
},
"id": "if_deadline",
"name": "Has Deadlines?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1150,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notification_queue (tenant_id, application_id, notification_type, priority, subject, body_html, body_text) VALUES ('{{ $('Loop Over Tenants').item.json.tenant_id }}', '{{ $json.id }}',\n 'deadline_approaching',\n 'critical',\n '{{ $json.subject }}',\n '{{ $json.bodyHtml }}',\n 'Deadline for {{ $json.job_title }} at {{ $json.company_name }} in {{ $json.hours_remaining }} hours.');",
"options": {}
},
"id": "pg_queue_deadline",
"name": "Queue Deadline Alert",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1400,
300
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT id AS tenant_id, name AS tenant_name, notification_email, candidate_profile, search_config, email_config FROM tenants WHERE is_active = true",
"options": {}
},
"id": "fetch_tenants",
"name": "Fetch Active Tenants",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
250,
0
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"id": "loop_tenants",
"name": "Loop Over Tenants",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
500,
0
]
}
],
"connections": {
"Every 6 Hours": {
"main": [
[
{
"node": "Fetch Active Tenants",
"type": "main",
"index": 0
}
]
]
},
"Fetch Active Applications": {
"main": [
[
{
"node": "Evaluate Follow-Up Status",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Follow-Up Status": {
"main": [
[
{
"node": "Switch Action",
"type": "main",
"index": 0
}
]
]
},
"Switch Action": {
"main": [
[
{
"node": "Generate Follow-Up Template",
"type": "main",
"index": 0
}
],
[
{
"node": "Transition to Ghosted",
"type": "main",
"index": 0
}
],
[]
]
},
"Generate Follow-Up Template": {
"main": [
[
{
"node": "Insert Follow-Up Log",
"type": "main",
"index": 0
}
]
]
},
"Insert Follow-Up Log": {
"main": [
[
{
"node": "Update Follow-Up Count",
"type": "main",
"index": 0
}
]
]
},
"Update Follow-Up Count": {
"main": [
[
{
"node": "Queue Follow-Up Notification",
"type": "main",
"index": 0
}
]
]
},
"Transition to Ghosted": {
"main": [
[
{
"node": "Queue Ghosting Notification",
"type": "main",
"index": 0
}
]
]
},
"Check Approaching Deadlines": {
"main": [
[
{
"node": "Format Deadline Alerts",
"type": "main",
"index": 0
}
]
]
},
"Format Deadline Alerts": {
"main": [
[
{
"node": "Has Deadlines?",
"type": "main",
"index": 0
}
]
]
},
"Has Deadlines?": {
"main": [
[
{
"node": "Queue Deadline Alert",
"type": "main",
"index": 0
}
],
[]
]
},
"Fetch Active Tenants": {
"main": [
[
{
"node": "Loop Over Tenants",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Tenants": {
"main": [
[],
[
{
"node": "Fetch Active Applications",
"type": "main",
"index": 0
}
]
]
},
"Queue Follow-Up Notification": {
"main": [
[
{
"node": "Loop Over Tenants",
"type": "main",
"index": 0
}
]
]
},
"Queue Ghosting Notification": {
"main": [
[
{
"node": "Loop Over Tenants",
"type": "main",
"index": 0
}
]
]
},
"Queue Deadline Alert": {
"main": [
[
{
"node": "Loop Over Tenants",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"timezone": "Europe/London",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": true
},
"staticData": {
"node:Every 6 Hours": {
"recurrenceRules": []
}
},
"meta": null,
"versionId": "2d03dec3-6495-4d83-85fb-af74f7725733",
"activeVersionId": "2d03dec3-6495-4d83-85fb-af74f7725733",
"versionCounter": 5,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-04-02T17:20:27.976Z",
"createdAt": "2026-04-02T17:20:27.976Z",
"role": "workflow:owner",
"workflowId": "RjELbd8jHpE0mYLy",
"projectId": "IrY2W58JPTTW41XY",
"project": {
"updatedAt": "2026-03-29T09:57:50.698Z",
"createdAt": "2026-03-29T09:50:40.962Z",
"id": "IrY2W58JPTTW41XY",
"name": "Venkatesan Ramachandran <venkat.fts@gmail.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "509e77ae-43b3-42df-bd9d-6e2a7aa26079"
}
}
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-04-02T17:20:27.983Z",
"createdAt": "2026-04-02T17:20:27.983Z",
"versionId": "2d03dec3-6495-4d83-85fb-af74f7725733",
"workflowId": "RjELbd8jHpE0mYLy",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8,14,20 * * *"
}
]
}
},
"id": "sched",
"name": "Every 6 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT a.id, a.company_name, a.job_title, a.pipeline_track, a.current_state,\n EXTRACT(EPOCH FROM (NOW() - a.state_changed_at)) / 86400 AS days_in_state,\n EXTRACT(EPOCH FROM (NOW() - a.applied_at)) / 86400 AS days_since_applied,\n a.follow_up_count,\n a.last_follow_up_at,\n a.follow_up_snoozed_until,\n a.applied_at,\n a.state_changed_at,\n a.reference_number\nFROM applications a\nWHERE a.is_active = true\n AND a.current_state IN ('applied', 'acknowledged', 'screening', 'interviewing', 'academic_longlisted', 'academic_shortlisted')\n AND (a.follow_up_snoozed_until IS NULL OR a.follow_up_snoozed_until < NOW())\nORDER BY a.state_changed_at ASC;",
"options": {}
},
"id": "pg_fetch",
"name": "Fetch Active Applications",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
250,
0
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"jsCode": "const apps = $input.all();\nconst results = [];\n\nfor (const app of apps) {\n const a = app.json;\n const track = a.pipeline_track;\n const state = a.current_state;\n const daysInState = parseFloat(a.days_in_state) || 0;\n const followUps = a.follow_up_count || 0;\n\n const thresholds = {\n corporate: {\n applied: { warn: 7, ghost: 14, maxFollowups: 2 },\n acknowledged: { warn: 10, ghost: 21, maxFollowups: 1 },\n screening: { warn: 7, ghost: 14, maxFollowups: 1 },\n interviewing: { warn: 7, ghost: 14, maxFollowups: 1 },\n },\n academic: {\n applied: { warn: 21, ghost: 56, maxFollowups: 1 },\n acknowledged: { warn: 28, ghost: 56, maxFollowups: 1 },\n academic_longlisted: { warn: 28, ghost: 56, maxFollowups: 1 },\n academic_shortlisted: { warn: 21, ghost: 42, maxFollowups: 1 },\n screening: { warn: 14, ghost: 28, maxFollowups: 1 },\n interviewing: { warn: 21, ghost: 42, maxFollowups: 1 },\n }\n };\n\n const t = ((thresholds[track] || thresholds.corporate)[state]);\n if (!t) {\n results.push({ json: { ...a, action: 'ok' } });\n continue;\n }\n\n let action = 'ok';\n if (daysInState >= t.ghost && followUps >= t.maxFollowups) {\n action = 'ghosted';\n } else if (daysInState >= t.warn && followUps < t.maxFollowups) {\n action = 'follow_up_due';\n }\n\n const reminderType = followUps === 0 ? 'initial_follow_up' : 'second_follow_up';\n\n results.push({ json: { ...a, action, thresholdDays: t.warn, ghostDays: t.ghost, reminderType, daysInState: Math.round(daysInState) } });\n}\n\nreturn results;"
},
"id": "code_eval",
"name": "Evaluate Follow-Up Status",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
500,
0
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "follow_up_due",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "follow_up_due"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "ghosted",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "ghosted"
}
]
},
"options": {
"allMatchingOutputs": false,
"fallbackOutput": "extra"
}
},
"id": "switch_action",
"name": "Switch Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
750,
0
]
},
{
"parameters": {
"jsCode": "const a = $input.first().json;\nconst company = a.company_name;\nconst title = a.job_title;\nconst days = a.daysInState;\nconst track = a.pipeline_track;\nconst state = a.current_state;\nconst ref = a.reference_number ? ` (Ref: ${a.reference_number})` : '';\n\nlet suggestedAction = '';\nlet template = '';\n\nif (track === 'academic') {\n suggestedAction = `Check university HR portal for status update. Academic hiring timelines are longer \u2014 ${days} days is approaching the follow-up threshold.`;\n template = `Dear Hiring Committee,\\n\\nI submitted my application for the ${title} position${ref} and wanted to confirm it was received. I remain very interested in this opportunity and would welcome any update on the timeline.\\n\\nKind regards,\\nSelvi`;\n} else {\n suggestedAction = `Send a polite follow-up email to ${company}. You applied ${days} days ago with no response.`;\n template = `Dear Hiring Manager,\\n\\nI applied for the ${title} role${ref} at ${company} and wanted to follow up. I'm very interested in this opportunity and would appreciate any update on the status of my application.\\n\\nBest regards,\\nSelvi`;\n}\n\nconst subject = `Follow-up needed: ${title} at ${company} (${days} days)`;\nconst bodyHtml = `<h3>Follow-Up Reminder</h3><p><strong>${title}</strong> at <strong>${company}</strong></p><p>You applied ${days} days ago (${track} track, state: ${state}).</p><p><strong>Suggested action:</strong> ${suggestedAction}</p><h4>Suggested follow-up email:</h4><pre>${template}</pre>`;\n\nreturn [{ json: { ...a, subject, bodyHtml, suggestedAction, template } }];"
},
"id": "code_followup",
"name": "Generate Follow-Up Template",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1000,
-100
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO follow_up_log (application_id, reminder_type, suggested_action, follow_up_template, sent_at)\nVALUES ('{{ $json.id }}', '{{ $json.reminderType }}', '{{ $json.suggestedAction }}', '{{ $json.template }}', NOW());",
"options": {}
},
"id": "pg_followup_log",
"name": "Insert Follow-Up Log",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1250,
-100
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE applications SET\n follow_up_count = follow_up_count + 1,\n last_follow_up_at = NOW(),\n next_follow_up_at = NOW() + INTERVAL '7 days'\nWHERE id = '{{ $json.id }}';",
"options": {}
},
"id": "pg_update_followup",
"name": "Update Follow-Up Count",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1500,
-100
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notification_queue (application_id, notification_type, priority, subject, body_html, body_text)\nVALUES (\n '{{ $json.id }}',\n 'follow_up_reminder',\n 'high',\n '{{ $json.subject }}',\n '{{ $json.bodyHtml }}',\n '{{ $json.suggestedAction }}'\n);",
"options": {}
},
"id": "pg_queue_followup",
"name": "Queue Follow-Up Notification",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1750,
-100
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT transition_application_state(\n '{{ $json.id }}'::uuid,\n 'ghosted',\n 'ghosting_detected',\n 'wf4_ghost',\n '{\"days_waited\": {{ $json.daysInState }}, \"follow_ups_sent\": {{ $json.follow_up_count }}}'::jsonb,\n 'Automatically detected as ghosted after {{ $json.daysInState }} days with no response'\n);",
"options": {}
},
"id": "pg_ghost_transition",
"name": "Transition to Ghosted",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1000,
100
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notification_queue (application_id, notification_type, priority, subject, body_html, body_text)\nVALUES (\n '{{ $json.id }}',\n 'ghosting_detected',\n 'medium',\n 'Ghosted: {{ $json.job_title }} at {{ $json.company_name }}',\n '<h3>Application Marked as Ghosted</h3><p><strong>{{ $json.job_title }}</strong> at <strong>{{ $json.company_name }}</strong></p><p>After {{ $json.daysInState }} days with no response and {{ $json.follow_up_count }} follow-up(s) sent, this application has been automatically marked as ghosted.</p><p>Pipeline track: {{ $json.pipeline_track }}</p>',\n 'Application to {{ $json.company_name }} for {{ $json.job_title }} marked as ghosted after {{ $json.daysInState }} days.'\n);",
"options": {}
},
"id": "pg_queue_ghost",
"name": "Queue Ghosting Notification",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1250,
100
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT a.id, a.company_name, a.job_title, a.current_state, j.expires_at,\n EXTRACT(EPOCH FROM (j.expires_at - NOW())) / 3600 AS hours_until_deadline\nFROM applications a\nJOIN jobs j ON a.job_id = j.id\nWHERE a.current_state IN ('discovered', 'shortlisted', 'cv_tailored')\n AND j.expires_at BETWEEN NOW() AND NOW() + INTERVAL '48 hours'\n AND j.expires_at > NOW();",
"options": {}
},
"id": "pg_deadlines",
"name": "Check Approaching Deadlines",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
250,
300
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst results = [];\nfor (const item of items) {\n const a = item.json;\n if (!a.id) continue;\n const hours = Math.round(parseFloat(a.hours_until_deadline) || 0);\n results.push({ json: {\n ...a,\n hours_remaining: hours,\n subject: `DEADLINE: ${a.job_title} at ${a.company_name} expires in ${hours} hours`,\n bodyHtml: `<h3>Application Deadline Approaching</h3><p><strong>${a.job_title}</strong> at <strong>${a.company_name}</strong></p><p>Deadline in <strong>${hours} hours</strong>.</p><p>Current status: ${a.current_state}. Apply now or mark as expired.</p>`\n }});\n}\nreturn results.length > 0 ? results : [{ json: { skip: true } }];"
},
"id": "code_deadline",
"name": "Format Deadline Alerts",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
500,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.skip }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notTrue"
}
}
],
"combinator": "and"
}
},
"id": "if_deadline",
"name": "Has Deadlines?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
750,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO notification_queue (application_id, notification_type, priority, subject, body_html, body_text)\nVALUES (\n '{{ $json.id }}',\n 'deadline_approaching',\n 'critical',\n '{{ $json.subject }}',\n '{{ $json.bodyHtml }}',\n 'Deadline for {{ $json.job_title }} at {{ $json.company_name }} in {{ $json.hours_remaining }} hours.'\n);",
"options": {}
},
"id": "pg_queue_deadline",
"name": "Queue Deadline Alert",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1000,
300
],
"credentials": {
"postgres": {
"id": "uAbCv6KI1KdUiMtX",
"name": "Selvi Jobs DB"
}
}
}
],
"connections": {
"Every 6 Hours": {
"main": [
[
{
"node": "Fetch Active Applications",
"type": "main",
"index": 0
},
{
"node": "Check Approaching Deadlines",
"type": "main",
"index": 0
}
]
]
},
"Fetch Active Applications": {
"main": [
[
{
"node": "Evaluate Follow-Up Status",
"type": "main",
"index": 0
}
]
]
},
"Evaluate Follow-Up Status": {
"main": [
[
{
"node": "Switch Action",
"type": "main",
"index": 0
}
]
]
},
"Switch Action": {
"main": [
[
{
"node": "Generate Follow-Up Template",
"type": "main",
"index": 0
}
],
[
{
"node": "Transition to Ghosted",
"type": "main",
"index": 0
}
],
[]
]
},
"Generate Follow-Up Template": {
"main": [
[
{
"node": "Insert Follow-Up Log",
"type": "main",
"index": 0
}
]
]
},
"Insert Follow-Up Log": {
"main": [
[
{
"node": "Update Follow-Up Count",
"type": "main",
"index": 0
}
]
]
},
"Update Follow-Up Count": {
"main": [
[
{
"node": "Queue Follow-Up Notification",
"type": "main",
"index": 0
}
]
]
},
"Transition to Ghosted": {
"main": [
[
{
"node": "Queue Ghosting Notification",
"type": "main",
"index": 0
}
]
]
},
"Check Approaching Deadlines": {
"main": [
[
{
"node": "Format Deadline Alerts",
"type": "main",
"index": 0
}
]
]
},
"Format Deadline Alerts": {
"main": [
[
{
"node": "Has Deadlines?",
"type": "main",
"index": 0
}
]
]
},
"Has Deadlines?": {
"main": [
[
{
"node": "Queue Deadline Alert",
"type": "main",
"index": 0
}
],
[]
]
}
},
"authors": "Venkatesan Ramachandran",
"name": null,
"description": null,
"autosaved": false,
"workflowPublishHistory": [
{
"createdAt": "2026-04-02T17:24:32.099Z",
"id": 55,
"workflowId": "RjELbd8jHpE0mYLy",
"versionId": "2d03dec3-6495-4d83-85fb-af74f7725733",
"event": "activated",
"userId": "509e77ae-43b3-42df-bd9d-6e2a7aa26079"
}
]
}
}
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.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
WF4-GHOST: Ghosting & Follow-Up Engine. Uses postgres. Scheduled trigger; 16 nodes.
Source: https://github.com/cto-venkat/selvi-job-app/blob/499ca24d9c9382b5f5561a096360564965fb4b8e/workflows/wf4-ghost-mt.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.
Disparador 1.8. Uses itemLists, postgres, emailSend, httpRequest. Scheduled trigger; 85 nodes.
공유회_알림톡_크론. Uses postgres, httpRequest, n8n-nodes-solapi. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.