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": "Lodge Reply Sync",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 2
}
]
}
},
"id": "schedule",
"name": "Every 2 Hours",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
200,
300
]
},
{
"parameters": {
"method": "GET",
"url": "https://api.instantly.ai/api/v1/unibox/emails",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "={{ $env.INSTANTLY_API_KEY }}"
},
{
"name": "email_type",
"value": "received"
},
{
"name": "limit",
"value": "50"
}
]
},
"options": {}
},
"id": "fetch_replies",
"name": "Fetch Instantly Replies",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
420,
300
],
"notes": "Pulls recent received emails from Instantly's Unibox. Adjust endpoint if Instantly's API changes."
},
{
"parameters": {
"jsCode": "// Filter to only new replies we haven't processed.\n// We track processed IDs in a static variable (resets on workflow restart,\n// but that's fine \u2014 Supabase upsert handles deduplication).\nconst items = Array.isArray($json) ? $json : ($json.data || $json.emails || []);\nconst results = [];\n\nfor (const reply of items) {\n const fromEmail = (reply.from_email || reply.from || reply.sender || '').toLowerCase().trim();\n const body = reply.body || reply.text || reply.snippet || '';\n const subject = reply.subject || '';\n const receivedAt = reply.timestamp || reply.date || reply.received_at || new Date().toISOString();\n const messageId = reply.id || reply.message_id || '';\n\n if (!fromEmail || !body) continue;\n\n results.push({\n json: {\n from_email: fromEmail,\n body,\n subject,\n received_at: receivedAt,\n message_id: messageId\n }\n });\n}\n\nif (results.length === 0) {\n // Return empty to stop the workflow gracefully\n return [{ json: { _empty: true } }];\n}\n\nreturn results;"
},
"id": "parse_replies",
"name": "Parse Replies",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
300
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json._empty }}",
"operation": "notEqual",
"value2": true
}
]
}
},
"id": "has_replies",
"name": "Has Replies?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
860,
300
]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/prospects",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "email",
"value": "=eq.{{ $json.from_email }}"
},
{
"name": "select",
"value": "id,business_name,status,vertical"
},
{
"name": "limit",
"value": "1"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
}
]
},
"options": {}
},
"id": "match_prospect",
"name": "Match Prospect by Email",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1080,
240
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Merge reply data with matched prospect\nconst reply = $('Has Replies?').item.json;\nconst prospects = $json;\nconst prospect = Array.isArray(prospects) ? prospects[0] : prospects;\n\nif (!prospect || !prospect.id) {\n // No matching prospect found \u2014 skip\n return { json: { skip: true, reason: 'no matching prospect', from_email: reply.from_email } };\n}\n\nreturn {\n json: {\n skip: false,\n prospect_id: prospect.id,\n business_name: prospect.business_name,\n current_status: prospect.status,\n vertical: prospect.vertical,\n from_email: reply.from_email,\n reply_body: reply.body,\n reply_subject: reply.subject,\n received_at: reply.received_at,\n message_id: reply.message_id\n }\n};"
},
"id": "merge_data",
"name": "Merge Reply + Prospect",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1300,
240
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.skip }}",
"value2": false
}
]
}
},
"id": "found_prospect",
"name": "Prospect Found?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1520,
240
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "content-type",
"value": "application/json"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"model\": \"claude-haiku-4-5-20251001\",\n \"max_tokens\": 200,\n \"system\": \"Classify this email reply into exactly ONE category. Reply with ONLY the category name, nothing else.\\n\\nCategories:\\n- positive_reply (interested, asks questions, wants to learn more, warm tone)\\n- booked (explicitly agrees to a call/meeting, provides times, confirms)\\n- not_interested (explicitly declines, says no thanks, not a fit)\\n- unsubscribe (asks to be removed, stop emailing, opt out)\\n- out_of_office (auto-reply, vacation, OOO)\\n- neutral (unclear intent, vague response, neither positive nor negative)\",\n \"messages\": [{\"role\": \"user\", \"content\": {{ JSON.stringify('Reply from ' + $json.business_name + ':\\n\\n' + $json.reply_body) }}}]\n}",
"options": {}
},
"id": "classify",
"name": "Claude: Classify Reply",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1740,
180
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"notes": "Uses Haiku for classification \u2014 fast and cheap. Only needs one word output."
},
{
"parameters": {
"jsCode": "// Map classification to CRM status update\nconst data = $('Prospect Found?').item.json;\nconst raw = ($json.content?.[0]?.text || '').trim().toLowerCase();\n\n// Normalize classification\nconst validTypes = ['positive_reply', 'booked', 'not_interested', 'unsubscribe', 'out_of_office', 'neutral'];\nconst classification = validTypes.find(t => raw.includes(t.replace('_', ' ')) || raw.includes(t)) || 'neutral';\n\n// Map to CRM prospect status\nconst statusMap = {\n positive_reply: 'active_lead',\n booked: 'active_lead',\n not_interested: 'closed_lost',\n unsubscribe: 'do_not_contact',\n out_of_office: data.current_status, // don't change status for OOO\n neutral: data.current_status\n};\n\nconst newStatus = statusMap[classification] || data.current_status;\n\nreturn {\n json: {\n ...data,\n classification,\n new_status: newStatus,\n should_update: newStatus !== data.current_status\n }\n};"
},
"id": "map_status",
"name": "Map to CRM Status",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1960,
180
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/responses",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"prospect_id\": {{ $json.prospect_id }},\n \"response_type\": {{ JSON.stringify($json.classification) }},\n \"subject\": {{ JSON.stringify($json.reply_subject || '') }},\n \"body\": {{ JSON.stringify($json.reply_body || '') }},\n \"from_email\": {{ JSON.stringify($json.from_email) }},\n \"received_at\": {{ JSON.stringify($json.received_at) }},\n \"gmail_id\": {{ JSON.stringify($json.message_id || '') }}\n}",
"options": {}
},
"id": "insert_response",
"name": "Insert Response to CRM",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2180,
180
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.should_update }}",
"value2": true
}
]
}
},
"id": "should_update",
"name": "Status Changed?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
2400,
180
]
},
{
"parameters": {
"method": "PATCH",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/prospects?id=eq.{{ $('Map to CRM Status').item.json.prospect_id }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={ \"status\": {{ JSON.stringify($('Map to CRM Status').item.json.new_status) }} }",
"options": {}
},
"id": "update_prospect",
"name": "Update Prospect Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2620,
120
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/activity_log",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=minimal"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"prospect_id\": {{ $('Map to CRM Status').item.json.prospect_id }},\n \"event_type\": \"reply_received\",\n \"event_data\": {\n \"classification\": {{ JSON.stringify($('Map to CRM Status').item.json.classification) }},\n \"from\": {{ JSON.stringify($('Map to CRM Status').item.json.from_email) }},\n \"business_name\": {{ JSON.stringify($('Map to CRM Status').item.json.business_name) }},\n \"new_status\": {{ JSON.stringify($('Map to CRM Status').item.json.new_status) }},\n \"source\": \"instantly_sync\"\n }\n}",
"options": {}
},
"id": "log_activity",
"name": "Log Activity",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2180,
340
],
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Every 2 Hours": {
"main": [
[
{
"node": "Fetch Instantly Replies",
"type": "main",
"index": 0
}
]
]
},
"Fetch Instantly Replies": {
"main": [
[
{
"node": "Parse Replies",
"type": "main",
"index": 0
}
]
]
},
"Parse Replies": {
"main": [
[
{
"node": "Has Replies?",
"type": "main",
"index": 0
}
]
]
},
"Has Replies?": {
"main": [
[
{
"node": "Match Prospect by Email",
"type": "main",
"index": 0
}
],
[]
]
},
"Match Prospect by Email": {
"main": [
[
{
"node": "Merge Reply + Prospect",
"type": "main",
"index": 0
}
]
]
},
"Merge Reply + Prospect": {
"main": [
[
{
"node": "Prospect Found?",
"type": "main",
"index": 0
}
]
]
},
"Prospect Found?": {
"main": [
[
{
"node": "Claude: Classify Reply",
"type": "main",
"index": 0
}
],
[]
]
},
"Claude: Classify Reply": {
"main": [
[
{
"node": "Map to CRM Status",
"type": "main",
"index": 0
}
]
]
},
"Map to CRM Status": {
"main": [
[
{
"node": "Insert Response to CRM",
"type": "main",
"index": 0
}
]
]
},
"Insert Response to CRM": {
"main": [
[
{
"node": "Status Changed?",
"type": "main",
"index": 0
},
{
"node": "Log Activity",
"type": "main",
"index": 0
}
]
]
},
"Status Changed?": {
"main": [
[
{
"node": "Update Prospect Status",
"type": "main",
"index": 0
}
],
[]
]
}
},
"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.
httpHeaderAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Lodge Reply Sync. Uses httpRequest. Scheduled trigger; 13 nodes.
Source: https://github.com/Isaac-Walden21/twenty1-crm/blob/9c78ed02caabd53b48df0e8f250ca9f60adeb254/n8n/reply-sync.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.
As n8n instances scale, teams often lose track of sub-workflows—who uses them, where they are referenced, and whether they can be safely updated. This leads to inefficiencies like unnecessary copies o
This workflow is an improvement of this workflow by Greg Brzezinka.
N8N-Workflow-Github-Manager. Uses github, httpRequest, n8n. Scheduled trigger; 38 nodes.
This workflow uses KlickTipp community nodes, available for self-hosted n8n instances only.
This workflow acts as an automated engagement bot. It sends a Direct Message (DM) with a link or resource to any follower who replies to your post with a specific target keyword.