AutomationFlowsWeb Scraping › Sync Lodge Reply Emails to Prospects

Sync Lodge Reply Emails to Prospects

Original n8n title: Lodge Reply Sync

Lodge Reply Sync. Uses httpRequest. Scheduled trigger; 13 nodes.

Cron / scheduled trigger★★★★☆ complexity13 nodesHTTP Request
Web Scraping Trigger: Cron / scheduled Nodes: 13 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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.

Pro

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 →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

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

HTTP Request, n8n, N8N Trigger +1
Web Scraping

This workflow is an improvement of this workflow by Greg Brzezinka.

HTTP Request, Email Send, XML +1
Web Scraping

N8N-Workflow-Github-Manager. Uses github, httpRequest, n8n. Scheduled trigger; 38 nodes.

GitHub, HTTP Request, n8n
Web Scraping

This workflow uses KlickTipp community nodes, available for self-hosted n8n instances only.

N8N Nodes Klicktipp, Salesforce, Salesforce Trigger +1
Web Scraping

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.

HTTP Request