AutomationFlowsWeb Scraping › Wholestack Wf-004: Follow-up Engine

Wholestack Wf-004: Follow-up Engine

WholeStack WF-004: Follow-Up Engine. Uses httpRequest. Scheduled trigger; 18 nodes.

Cron / scheduled trigger★★★★☆ complexity18 nodesHTTP Request
Web Scraping Trigger: Cron / scheduled Nodes: 18 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": "WholeStack WF-004: Follow-Up Engine",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": null
            }
          ]
        }
      },
      "id": "schedule",
      "name": "Every 30 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        256,
        304
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/getDueFollowUps",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ current_time: Date.now(), max_follow_ups: 4, active_statuses: ['ping_sent', 'details_sent', 'locked'] }) }}",
        "options": {}
      },
      "id": "getDue",
      "name": "Get Due Follow-Ups",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        304
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond1",
              "leftValue": "={{ $json.data.length }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "ifHasDue",
      "name": "IF Has Due Follow-Ups",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        704,
        304
      ]
    },
    {
      "parameters": {},
      "id": "noOp",
      "name": "No Follow-Ups Due",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        912,
        464
      ]
    },
    {
      "parameters": {
        "jsCode": "var distributions = $input.first().json.data || [];\nvar results = [];\n\nfor (var i = 0; i < distributions.length; i++) {\n  var dist = distributions[i];\n  var deal = dist.deal || {};\n  var buyer = dist.buyer || {};\n  \n  var skipStatuses = ['sold', 'expired', 'dead', 'under_contract'];\n  var shouldSkip = deal.status && skipStatuses.indexOf(deal.status) !== -1;\n  \n  if (!shouldSkip) {\n    results.push({ json: { distribution: dist, deal: deal, buyer: buyer } });\n  }\n}\n\nreturn results;"
      },
      "id": "prepareArray",
      "name": "Prepare Distributions Array",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        912,
        160
      ]
    },
    {
      "parameters": {
        "jsCode": "var distribution = $input.first().json.distribution;\nvar deal = $input.first().json.deal;\nvar buyer = $input.first().json.buyer;\n\nvar timezone = (buyer.preferences && buyer.preferences.timezone) || 'America/New_York';\n\nvar now = new Date();\nvar currentHour = now.getUTCHours();\n\nvar tzOffsets = {\n  'America/New_York': -4,\n  'America/Chicago': -5,\n  'America/Denver': -6,\n  'America/Los_Angeles': -7\n};\nvar offset = tzOffsets[timezone] || -4;\nvar localHour = (currentHour + offset + 24) % 24;\n\nvar isBusinessHours = localHour >= 9 && localHour < 20;\n\nreturn { json: { distribution: distribution, deal: deal, buyer: buyer, isBusinessHours: isBusinessHours } };"
      },
      "id": "checkBizHours",
      "name": "Check Business Hours",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1136,
        160
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond2",
              "leftValue": "={{ $json.isBusinessHours }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "ifBizHours",
      "name": "IF Within Business Hours",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1360,
        160
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/updateDistributionFollowUp",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ distributionId: $json.distribution._id, nextFollowUp: Date.now() + (12 * 60 * 60 * 1000) }) }}",
        "options": {}
      },
      "id": "reschedule",
      "name": "Reschedule to 9 AM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1584,
        304
      ]
    },
    {
      "parameters": {
        "jsCode": "var dist = $json;\nvar status = dist.status;\nvar followUpCount = dist.follow_up_count || 0;\nvar buyerName = dist.buyer ? dist.buyer.display_name.split(' ')[0] : 'Investor';\nvar dealAddress = dist.deal ? dist.deal.property_address : 'the property';\nvar askingPrice = dist.deal ? dist.deal.asking_price : 0;\n\nvar template = '';\nvar isLastFollowUp = followUpCount >= 3;\n\nif (status === 'ping_sent') {\n  if (followUpCount === 0) {\n    template = buyerName + ', still interested in ' + dealAddress + '? Reply YES for full details. -Team';\n  } else if (followUpCount === 1) {\n    template = buyerName + ' - 2 other buyers looking at ' + dealAddress + '. Want details before they lock it? Reply YES. -Team';\n  } else if (followUpCount === 2) {\n    template = 'Hey ' + buyerName + ', getting offers on ' + dealAddress + '. Need your answer today. YES or PASS? -Team';\n  } else {\n    template = 'FINAL NOTICE ' + buyerName + ': ' + dealAddress + ' - passing to next buyer in 2 hours. Last chance to reply YES. -Team';\n  }\n} else if (status === 'details_sent') {\n  if (followUpCount === 0) {\n    template = buyerName + ', thoughts on ' + dealAddress + '? Reply LOCK to hold it for 24hrs. -Team';\n  } else if (followUpCount === 1) {\n    template = buyerName + ' - another buyer is reviewing ' + dealAddress + '. Lock it now or lose it? Reply LOCK. -Team';\n  } else if (followUpCount === 2) {\n    template = 'Hey ' + buyerName + ', need a decision on ' + dealAddress + ' today. LOCK or PASS? -Team';\n  } else {\n    template = 'FINAL: ' + dealAddress + ' goes to next buyer in 2 hours. Reply LOCK now or it\\'s gone. -Team';\n  }\n} else if (status === 'locked') {\n  if (followUpCount === 0) {\n    template = buyerName + ', you locked ' + dealAddress + '. Need your email to send the package! -Team';\n  } else if (followUpCount === 1) {\n    template = buyerName + ' - still need your email for the ' + dealAddress + ' package. Reply with your email address. -Team';\n  } else if (followUpCount === 2) {\n    template = 'Hey ' + buyerName + ', your hold on ' + dealAddress + ' expires soon. Send your email to get the deal package! -Team';\n  } else {\n    template = 'URGENT ' + buyerName + ': Hold expiring on ' + dealAddress + '. Reply with EMAIL now or we release it. -Team';\n  }\n}\n\nvar nextFollowUp = isLastFollowUp ? null : Date.now() + (4 * 60 * 60 * 1000);\n\nreturn {\n  json: {\n    ...dist,\n    smsText: template,\n    nextFollowUp: nextFollowUp,\n    isLastFollowUp: isLastFollowUp,\n    followUpNumber: followUpCount + 1\n  }\n};"
      },
      "id": "selectTemplate",
      "name": "Select Follow-Up Template",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1584,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/activity/sms/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $json.buyer.closeLeadId, local_phone: '+15555550100', remote_phone: $json.buyer.phone, text: $json.smsText, direction: 'outbound', status: 'outbox' }) }}",
        "options": {}
      },
      "id": "sendSms",
      "name": "Send Follow-Up SMS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1792,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/updateDistributionFollowUp",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ distributionId: $('Select Follow-Up Template').first().json.distribution._id, followUpCount: ($('Select Follow-Up Template').first().json.distribution.followUpCount || 0) + 1, lastFollowUp: Date.now(), nextFollowUp: $('Select Follow-Up Template').first().json.isLastFollowUp ? null : Date.now() + (48 * 60 * 60 * 1000) }) }}",
        "options": {}
      },
      "id": "updateFollowUp",
      "name": "Update Follow-Up Count",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2016,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond3",
              "leftValue": "={{ $('Select Follow-Up Template').first().json.isLastFollowUp }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "ifLastFollowUp",
      "name": "IF Last Follow-Up",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2240,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/updateDistributionStatus",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ distributionId: $('Select Follow-Up Template').first().json.distribution._id, status: 'no_response' }) }}",
        "options": {}
      },
      "id": "markExhausted",
      "name": "Mark Distribution Exhausted",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2464,
        -96
      ]
    },
    {
      "parameters": {
        "amount": 1
      },
      "id": "wait",
      "name": "Rate Limit Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        2464,
        160
      ]
    },
    {
      "id": "get-close-status",
      "name": "Get Close Lead Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1680,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.close.com/api/v1/lead/{{ $json.buyer.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "check-protected",
      "name": "Check Protected Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1730,
        0
      ],
      "parameters": {
        "jsCode": "// Check if lead has protected status\nvar closeData = $json;\nvar templateData = $('Select Follow-Up Template').first().json;\nvar statusLabel = (closeData.status_label || '').toLowerCase().trim();\n\n// PROTECTED STATUSES - NO SMS for these\nvar protectedStatuses = [\n  'human hand', 'hot lead', 'hot', 'warm lead',\n  'interested', 'not interested', 'dnc', 'dead',\n  'vetted buyer', 'vip'\n];\n\nvar isProtected = protectedStatuses.indexOf(statusLabel) !== -1;\n\nreturn [{\n  json: {\n    ...templateData,\n    _closeStatus: statusLabel,\n    _isProtectedStatus: isProtected,\n    canSend: !isProtected\n  }\n}];"
      }
    },
    {
      "id": "is-protected",
      "name": "Is Protected?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1730,
        100
      ],
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "protected-check",
              "leftValue": "={{ $json.canSend }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and",
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          }
        },
        "options": {}
      }
    },
    {
      "id": "skip-protected",
      "name": "Skip Protected",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1800,
        200
      ],
      "parameters": {}
    }
  ],
  "connections": {
    "Every 30 Minutes": {
      "main": [
        [
          {
            "node": "Get Due Follow-Ups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Due Follow-Ups": {
      "main": [
        [
          {
            "node": "IF Has Due Follow-Ups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Has Due Follow-Ups": {
      "main": [
        [
          {
            "node": "Prepare Distributions Array",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Follow-Ups Due",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Distributions Array": {
      "main": [
        [
          {
            "node": "Check Business Hours",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Business Hours": {
      "main": [
        [
          {
            "node": "IF Within Business Hours",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Within Business Hours": {
      "main": [
        [
          {
            "node": "Select Follow-Up Template",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reschedule to 9 AM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Follow-Up Template": {
      "main": [
        [
          {
            "node": "Get Close Lead Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Close Lead Status": {
      "main": [
        [
          {
            "node": "Check Protected Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Protected Status": {
      "main": [
        [
          {
            "node": "Is Protected?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Protected?": {
      "main": [
        [
          {
            "node": "Send Follow-Up SMS",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip Protected",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Follow-Up SMS": {
      "main": [
        [
          {
            "node": "Update Follow-Up Count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Follow-Up Count": {
      "main": [
        [
          {
            "node": "IF Last Follow-Up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Last Follow-Up": {
      "main": [
        [
          {
            "node": "Mark Distribution Exhausted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rate Limit Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Distribution Exhausted": {
      "main": [
        [
          {
            "node": "Rate Limit Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "binaryMode": "separate"
  },
  "active": false
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

WholeStack WF-004: Follow-Up Engine. Uses httpRequest. Scheduled trigger; 18 nodes.

Source: https://github.com/rafiulislam4246/real-estate-disposition-workflows/blob/main/workflows/04-followup-engine.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

Birthday Automation - Production (Fixed). Uses stopAndError, httpRequest, emailSend, bannerbear. Scheduled trigger; 86 nodes.

Stop And Error, HTTP Request, Email Send +1
Web Scraping

This template runs two scheduled workflows to govern Microsoft Entra ID (Azure AD) guest accounts by detecting stale users via Microsoft Graph, staging deletions in SharePoint with a 72-hour window, n

Microsoft SharePoint, Microsoft Teams, Microsoft Entra +1
Web Scraping

Jira-Allure-Auto-Qa. Uses httpRequest, jira. Scheduled trigger; 68 nodes.

HTTP Request, Jira
Web Scraping

Spotify-Sync-Surrealdb-V1. Uses httpRequest, n8n-nodes-surrealdb, spotify. Scheduled trigger; 62 nodes.

HTTP Request, N8N Nodes Surrealdb, Spotify
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