AutomationFlowsSlack & Telegram › Wholestack Wf-001: Deal Intake & Enrichment

Wholestack Wf-001: Deal Intake & Enrichment

WholeStack WF-001: Deal Intake & Enrichment. Uses httpRequest, slack. Webhook trigger; 10 nodes.

Webhook trigger★★★★☆ complexity10 nodesHTTP RequestSlack
Slack & Telegram Trigger: Webhook Nodes: 10 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Slack 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 →

Download .json
{
  "name": "WholeStack WF-001: Deal Intake & Enrichment",
  "nodes": [
    {
      "id": "webhook",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        0
      ],
      "parameters": {
        "path": "wholestack-deal-intake-v2",
        "httpMethod": "POST",
        "responseMode": "responseNode",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "validate",
      "name": "Validate Deal",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Get input - handle both direct JSON and webhook body\nconst rawInput = $input.first().json;\nconst input = rawInput.body || rawInput;\nconst errors = [];\n\n// Sanitize string fields - trim whitespace and normalize\nconst stringFields = ['address', 'city', 'state', 'zip', 'propertyType', 'repairScope', 'source', 'driveLink'];\nfor (const field of stringFields) {\n  if (typeof input[field] === 'string') {\n    input[field] = input[field].trim();\n  }\n}\n\n// Uppercase state (e.g. 'oh' -> 'OH')\nif (typeof input.state === 'string') {\n  input.state = input.state.toUpperCase();\n}\n\n// Required fields\nconst required = ['address', 'city', 'state', 'zip', 'askingPrice', 'arv', 'repairEstimate', 'repairScope', 'propertyType', 'source'];\nfor (const field of required) {\n  if (!input[field] && input[field] !== 0) {\n    errors.push('Missing required field: ' + field);\n  }\n}\n\n// Type validations\nif (input.askingPrice && input.askingPrice <= 0) errors.push('askingPrice must be > 0');\nif (input.arv && input.askingPrice && input.arv <= input.askingPrice) errors.push('arv must be > askingPrice');\nif (input.repairEstimate && input.repairEstimate < 0) errors.push('repairEstimate must be >= 0');\n\nconst validPropertyTypes = ['SFR', 'Duplex', 'Multi', 'Land', 'Commercial'];\nif (input.propertyType && !validPropertyTypes.includes(input.propertyType)) {\n  errors.push('propertyType must be one of: ' + validPropertyTypes.join(', '));\n}\n\nconst validScopes = ['Light', 'Medium', 'Heavy', 'Gut'];\nif (input.repairScope && !validScopes.includes(input.repairScope)) {\n  errors.push('repairScope must be one of: ' + validScopes.join(', '));\n}\n\nif (errors.length > 0) {\n  return [{json: {valid: false, errors}}];\n}\n\nreturn [{json: {valid: true, deal: input}}];"
      }
    },
    {
      "id": "ifValid",
      "name": "IF Valid",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        440,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "caseSensitive": true,
            "typeValidation": "strict",
            "leftValue": ""
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "valid-check",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": ""
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "respond400",
      "name": "Respond 400",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        660,
        150
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: false, errors: $json.errors }) }}",
        "options": {
          "responseCode": 400
        }
      }
    },
    {
      "id": "normalizeAddress",
      "name": "Normalize Address",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        -150
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const deal = $input.first().json.deal;\n\nfunction normalizeAddress(addr) {\n  var normalized = addr.toLowerCase().trim();\n  \n  // Standardize street suffixes\n  var replacements = {\n    ' st ': ' street ', ' st$': ' street',\n    ' ave ': ' avenue ', ' ave$': ' avenue',\n    ' dr ': ' drive ', ' dr$': ' drive',\n    ' rd ': ' road ', ' rd$': ' road',\n    ' blvd ': ' boulevard ', ' blvd$': ' boulevard',\n    ' ln ': ' lane ', ' ln$': ' lane',\n    ' ct ': ' court ', ' ct$': ' court',\n    ' cir ': ' circle ', ' cir$': ' circle',\n    ' pl ': ' place ', ' pl$': ' place'\n  };\n  \n  for (var abbr in replacements) {\n    normalized = normalized.replace(new RegExp(abbr, 'g'), replacements[abbr]);\n  }\n  \n  // Remove punctuation\n  normalized = normalized.replace(/[.,#]/g, '');\n  \n  // Remove unit/apt/suite\n  normalized = normalized.replace(/\\s+(apt|unit|suite|ste|#)\\s*\\S*/gi, '');\n  \n  // Collapse whitespace\n  normalized = normalized.replace(/\\s+/g, ' ').trim();\n  \n  return normalized;\n}\n\nvar addressNormalized = normalizeAddress(deal.address);\n\nreturn [{json: {...deal, addressNormalized: addressNormalized}}];"
      }
    },
    {
      "id": "calculateScore",
      "name": "Calculate Score",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        -150
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "var deal = $input.first().json;\n\n// Calculate margin\nvar margin = ((deal.arv - deal.askingPrice - deal.repairEstimate) / deal.arv) * 100;\n\n// Helper function\nfunction clamp(value, min, max) {\n  return Math.min(Math.max(value, min), max);\n}\n\n// Score components\nvar marginScore = clamp(margin / 40 * 100, 0, 100);\nvar demandScore = 50; // Default\n\nvar conditionScores = { Light: 90, Medium: 70, Heavy: 40, Gut: 20 };\nvar conditionScore = conditionScores[deal.repairScope] || 50;\n\nvar compConfidence = deal.compConfidence || 'Medium';\nvar compScores = { High: 100, Medium: 60, Low: 30 };\nvar compScore = compScores[compConfidence] || 60;\n\nvar domScore = 70;\nvar titleScore = 100;\n\n// Calculate overall deal score\nvar dealScore = Math.round(\n  marginScore * 0.30 +\n  demandScore * 0.20 +\n  domScore * 0.15 +\n  conditionScore * 0.15 +\n  compScore * 0.10 +\n  titleScore * 0.10\n);\n\n// Determine best strategy\nvar bestStrategy;\nif (margin >= 25 && ['Light', 'Medium'].indexOf(deal.repairScope) !== -1) {\n  bestStrategy = 'Flip';\n} else if (margin >= 20 && ['Medium', 'Heavy'].indexOf(deal.repairScope) !== -1) {\n  bestStrategy = 'BRRRR';\n} else if (margin >= 15) {\n  bestStrategy = 'Rental';\n} else {\n  bestStrategy = 'Wholesale';\n}\n\nreturn [{json: {\n  ...deal,\n  margin: Math.round(margin * 10) / 10,\n  dealScore: dealScore,\n  bestStrategy: bestStrategy,\n  compConfidence: compConfidence\n}}];"
      }
    },
    {
      "id": "storeDeal",
      "name": "Store Deal",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1100,
        -150
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT.convex.site/createDeal",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"property_address\": \"{{ $json.address }}, {{ $json.city }}, {{ $json.state }} {{ $json.zip }}\",\n  \"address_normalized\": \"{{ $json.addressNormalized }}\",\n  \"city\": \"{{ $json.city }}\",\n  \"state\": \"{{ $json.state }}\",\n  \"zip\": \"{{ $json.zip }}\",\n  \"property_type\": \"{{ $json.propertyType }}\",\n  \"beds\": {{ $json.beds || 0 }},\n  \"baths\": {{ $json.baths || 0 }},\n  \"sqft\": {{ $json.sqft || 0 }},\n  \"asking_price\": {{ $json.askingPrice }},\n  \"arv\": {{ $json.arv }},\n  \"repair_estimate\": {{ $json.repairEstimate }},\n  \"repair_scope\": \"{{ $json.repairScope }}\",\n  \"margin\": {{ $json.margin }},\n  \"deal_score\": {{ $json.dealScore }},\n  \"best_strategy\": \"{{ $json.bestStrategy }}\",\n  \"comp_confidence\": \"{{ $json.compConfidence }}\",\n  \"source_type\": \"{{ $json.source }}\",\n  \"photos\": {{ JSON.stringify($json.driveLink ? [$json.driveLink] : []) }}\n}",
        "options": {
          "timeout": 15000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "triggerDistribution",
      "name": "Trigger Distribution",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1320,
        -150
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_N8N_HOST/webhook/wholestack-distribute-deal",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"dealId\": \"{{ $('Store Deal').first().json.data._id }}\",\n  \"deal\": {{ JSON.stringify($('Calculate Score').first().json) }},\n  \"driveLink\": \"{{ $('Calculate Score').first().json.driveLink || '' }}\",\n  \"maxBuyers\": {{ $('Calculate Score').first().json.maxBuyers || 200 }}\n}",
        "options": {
          "timeout": 30000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "respondSuccess",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1760,
        -150
      ],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"dealId\": \"{{ $('Store Deal').first().json.data._id }}\",\n  \"dealScore\": {{ $('Calculate Score').first().json.dealScore }},\n  \"margin\": {{ $('Calculate Score').first().json.margin }},\n  \"bestStrategy\": \"{{ $('Calculate Score').first().json.bestStrategy }}\",\n  \"status\": \"ready\"\n}",
        "options": {
          "responseCode": 200
        }
      }
    },
    {
      "id": "slackNotify",
      "name": "Slack Notify",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1540,
        -150
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "your-slack-channel",
          "mode": "name"
        },
        "messageType": "text",
        "text": "=\ud83c\udfe0 New WholeStack deal: {{ $('Calculate Score').first().json.address }} | Score: {{ $('Calculate Score').first().json.dealScore }}/100 | ${{ $('Calculate Score').first().json.askingPrice }} | {{ $('Calculate Score').first().json.margin }}% margin | {{ $('Calculate Score').first().json.bestStrategy }}",
        "otherOptions": {}
      },
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Validate Deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Deal": {
      "main": [
        [
          {
            "node": "IF Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Valid": {
      "main": [
        [
          {
            "node": "Normalize Address",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 400",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Address": {
      "main": [
        [
          {
            "node": "Calculate Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Score": {
      "main": [
        [
          {
            "node": "Store Deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Deal": {
      "main": [
        [
          {
            "node": "Trigger Distribution",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger Distribution": {
      "main": [
        [
          {
            "node": "Slack Notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Notify": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false
  },
  "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-001: Deal Intake & Enrichment. Uses httpRequest, slack. Webhook trigger; 10 nodes.

Source: https://github.com/rafiulislam4246/real-estate-disposition-workflows/blob/main/workflows/01-deal-intake-enrichment.json — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

HR teams, IT Operations, and System Administrators managing employee onboarding at scale. It’s perfect if you use Odoo 18 to trigger account requests and need Redmine + GitLab accounts created instant

HTTP Request, Slack
Slack & Telegram

This workflow is a complete, production-ready solution for recovering abandoned carts in Shopify stores using a multi-channel, multi-touch approach. It automates personalized follow-ups via Email, SMS

HTTP Request, Shopify, SendGrid +5
Slack & Telegram

Backbrief: transcripts (Zoom webhook -> Slack + vault). Uses httpRequest, slack. Webhook trigger; 52 nodes.

HTTP Request, Slack
Slack & Telegram

This workflow automates end-to-end research analysis by coordinating multiple AI models—including NVIDIA NIM (Llama), OpenAI GPT-4, and Claude to analyze uploaded documents, extract insights, and gene

HTTP Request, Postgres, Slack +1
Slack & Telegram

Are you tired of the repetitive dance between git push, creating a pull request in GitHub, updating the corresponding task in JIRA, and then manually notifying your team in Slack, or Notion?

HTTP Request, Stop And Error, Jira +2