AutomationFlowsData & Sheets › Route Insurance Quote Leads with Openai, Airtable, Sheets, Teams, Slack and…

Route Insurance Quote Leads with Openai, Airtable, Sheets, Teams, Slack and…

Original n8n title: Route Insurance Quote Leads with Openai, Airtable, Sheets, Teams, Slack and Twilio

ByPrecision Tech @precision-tech on n8n.io

This workflow receives insurance quote submissions via a webhook, normalizes and deduplicates them against Airtable and/or Google Sheets, uses OpenAI to score and classify the lead, then routes hot/warm leads to Microsoft Teams and/or Slack (plus Twilio SMS for hot leads) and…

Webhook trigger★★★★★ complexityAI-powered56 nodesAirtableGoogle SheetsOpenAIHTTP RequestSlackTwilio
Data & Sheets Trigger: Webhook Nodes: 56 Complexity: ★★★★★ AI nodes: yes Added:

This workflow corresponds to n8n.io template #17075 — we link there as the canonical source.

This workflow follows the Airtable → Google Sheets 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
{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Insurance Quote Intake AI Router",
  "tags": [],
  "nodes": [
    {
      "id": "d42cdf54-9580-43ef-aabb-bff4baf891b4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -608,
        -32
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Insurance Quote Intake AI Router\n\n### How it works\n\nThis workflow receives an insurance quote form submission, normalizes it, checks existing Airtable and/or Google Sheets logs for duplicates, and then uses OpenAI to pre-qualify new leads. It routes hot and warm leads to Teams, Slack, and optionally Twilio SMS alerts, while cold leads go straight to logging. Finally, it writes the quote outcome to the configured log destinations and prepares a final response.\n\n### Setup steps\n\n- Configure the webhook trigger URL in the quote form or website that submits requests.\n- Edit the Setup Quote Configuration code node with business settings, routing flags, log destination IDs, alert toggles, phone numbers, and webhook URLs.\n- Connect OpenAI credentials and confirm the prompt/model settings match the desired quote pre-qualification criteria.\n- Connect Airtable and Google Sheets credentials if either lookup or write logging path is enabled, and verify base/table/sheet column names match the code mappings.\n- Configure Microsoft Teams incoming webhook, Slack credentials/channel, and Twilio credentials if those alert channels are enabled.\n\n### Customization\n\nAdjust the OpenAI qualification prompt, priority thresholds, duplicate-matching rules, alert message templates, and which channels or log destinations are enabled for different lead priorities."
      },
      "typeVersion": 1
    },
    {
      "id": "cfba182b-5064-45b5-a593-e0ca70a2aa34",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 352,
        "content": "## Receive quote submission\n\nWebhook entry point that receives the incoming insurance quote form payload."
      },
      "typeVersion": 1
    },
    {
      "id": "f30f5fbe-0a46-42fe-bfa4-33e2f2dee99e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Configure and normalize\n\nLoads editable workflow settings and standardizes the submitted quote request into the structure used downstream."
      },
      "typeVersion": 1
    },
    {
      "id": "487949e3-5f36-4af9-ab18-ad3c94aca8ea",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 512,
        "content": "## Airtable lookup branch\n\nConditionally reads prior quote records from Airtable, or builds an equivalent skip bundle when Airtable lookup is disabled."
      },
      "typeVersion": 1
    },
    {
      "id": "47e9cc20-4a68-4e95-a5cc-2bb521b060eb",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 544,
        "content": "## Sheets and AI hub\n\nA shared mid-canvas cluster that handles optional Google Sheets lookup/pass-through rows and also contains the later OpenAI pre-qualification, parsing, and priority switch for new quotes."
      },
      "typeVersion": 1
    },
    {
      "id": "e8590fb8-49c5-4d02-a887-2d8f252c954a",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2272,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 592,
        "content": "## Duplicate and route outputs\n\nVerifies whether the quote is new, gates processing for new submissions, and begins the spatially nearby hot/warm alert path or cold lead log path."
      },
      "typeVersion": 1
    },
    {
      "id": "ca190899-96d3-4b1d-9753-66e6b9f79b5a",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2832,
        -32
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 512,
        "content": "## Teams alert path\n\nPosts a prepared alert to Microsoft Teams when enabled, confirms delivery, or marks the Teams alert as skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "1f06ce0a-13d6-4861-a1bf-006774280715",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3328,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 368,
        "content": "## Slack alert decision\n\nChecks whether the workflow should continue by sending a Slack alert after the Teams branch completes or is skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "37009ed0-b913-4ea0-9ad1-ca744e012939",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3600,
        -32
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 512,
        "content": "## Slack alert path\n\nSends the Slack alert when enabled, records the send confirmation, or marks the Slack alert as skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "54c8bd71-31df-40bb-aae1-8b78d9beee36",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4096,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 336,
        "content": "## SMS need decision\n\nDetermines whether a hot lead requires an immediate SMS alert after team channel notifications finish."
      },
      "typeVersion": 1
    },
    {
      "id": "a0cb75a5-529a-4018-a582-1e44386cd241",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4384,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 496,
        "content": "## SMS delivery path\n\nPrepares and sends a Twilio SMS for hot leads, confirms delivery, or marks that no SMS was needed."
      },
      "typeVersion": 1
    },
    {
      "id": "02a7c15c-91c0-4c58-a290-8f5997619551",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4384,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 336,
        "content": "## Build log record\n\nCombines the lead analysis, alert outcomes, and quote details into a normalized record for persistence."
      },
      "typeVersion": 1
    },
    {
      "id": "9866404b-883f-4c51-a791-1fb9ea1d1a8e",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4656,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 496,
        "content": "## Airtable write branch\n\nConditionally creates the final quote record in Airtable, confirms the write, or passes along a skipped-write marker."
      },
      "typeVersion": 1
    },
    {
      "id": "8bd429e6-d785-454b-9c88-304e0a30dee2",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5408,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 496,
        "content": "## Sheets write branch\n\nConditionally appends the final quote record to Google Sheets, confirms the write, or marks the Sheets write as skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "9fe881ca-bc55-405b-8a31-ea199425d47e",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6160,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 336,
        "content": "## Finalize quote response\n\nProduces the final workflow response after the logging branches have completed or been skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "77777777-7777-7777-8777-666666666601",
      "name": "When Quote Form Submitted",
      "type": "n8n-nodes-base.webhook",
      "position": [
        0,
        320
      ],
      "parameters": {
        "path": "insurance-quote-intake",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "lastNode"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666602",
      "name": "Configure Quote Settings",
      "type": "n8n-nodes-base.code",
      "position": [
        288,
        320
      ],
      "parameters": {
        "jsCode": "// ===== EDIT YOUR SETTINGS HERE =====\n// Defaults below match the Gumroad + n8n Creator listings.\n// Change only what you need; leave unused destinations as placeholders.\n\n// Where to log quotes:\n//   'airtable' \u2192 Airtable only\n//   'sheets'   \u2192 Google Sheets only\n//   'both'     \u2192 Airtable and Google Sheets\nconst QUOTE_LOG = 'airtable';\n\nconst AIRTABLE_BASE_ID = 'appXXXXXXXXXXXXXX';\nconst QUOTES_TABLE_NAME = 'Quotes';\n\n// Required when QUOTE_LOG is 'sheets' or 'both'\nconst GOOGLE_SHEET_ID = 'your-google-sheet-id';\nconst SHEETS_TAB_NAME = 'Quotes';\n\n// Where to send chat alerts for hot/warm leads:\n//   'teams'  \u2192 Microsoft Teams only\n//   'slack'  \u2192 Slack only\n//   'both'   \u2192 Teams and Slack\nconst NOTIFY_CHANNELS = 'teams';\n\n// Microsoft Teams incoming webhook URL (Channel \u2192 Connectors \u2192 Incoming Webhook)\n// Required when NOTIFY_CHANNELS is 'teams' or 'both'\nconst TEAMS_WEBHOOK_URL = 'https://outlook.office.com/webhook/your-teams-incoming-webhook';\n\n// Slack channel name (with #). Required when NOTIFY_CHANNELS is 'slack' or 'both'\nconst SLACK_CHANNEL = '#quote-leads';\n\n// Twilio SMS \u2014 hot leads only\nconst TWILIO_FROM_NUMBER = '+1234567890';\nconst AGENT_MOBILE_NUMBER = '+1234567890';\n\n// Lead score at or above this value triggers SMS + priority chat alerts\nconst HOT_LEAD_SCORE_THRESHOLD = 75;\n\n// Skip duplicate submissions with same email/phone within this window (hours)\nconst DUPLICATE_WINDOW_HOURS = 24;\n\nconst DEFAULT_LEAD_SOURCE = 'website_form';\n\nconst payload = $input.first().json.body || $input.first().json;\nconst quoteLog = String(QUOTE_LOG || 'airtable').toLowerCase().trim();\nconst useAirtableLog = quoteLog === 'airtable' || quoteLog === 'both';\nconst useSheetsLog = quoteLog === 'sheets' || quoteLog === 'both';\n\nreturn [\n  {\n    json: {\n      quoteLog,\n      useAirtableLog,\n      useSheetsLog,\n      airtableBaseId: AIRTABLE_BASE_ID,\n      quotesTableName: QUOTES_TABLE_NAME,\n      googleSheetId: GOOGLE_SHEET_ID,\n      sheetsTabName: SHEETS_TAB_NAME,\n      notifyChannels: NOTIFY_CHANNELS,\n      teamsWebhookUrl: TEAMS_WEBHOOK_URL,\n      slackChannel: SLACK_CHANNEL,\n      twilioFromNumber: TWILIO_FROM_NUMBER,\n      agentMobileNumber: AGENT_MOBILE_NUMBER,\n      hotLeadScoreThreshold: HOT_LEAD_SCORE_THRESHOLD,\n      duplicateWindowHours: DUPLICATE_WINDOW_HOURS,\n      defaultLeadSource: DEFAULT_LEAD_SOURCE,\n      payload,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666603",
      "name": "Normalize Quote Data",
      "type": "n8n-nodes-base.code",
      "position": [
        560,
        320
      ],
      "parameters": {
        "jsCode": "const settings = $input.first().json;\nconst raw = settings.payload || settings.body || settings;\n\nconst get = (...keys) => {\n  for (const key of keys) {\n    if (raw[key] !== undefined && raw[key] !== null && String(raw[key]).trim() !== '') {\n      return String(raw[key]).trim();\n    }\n  }\n  return '';\n};\n\nconst formResponse = raw.form_response || raw;\nconst answers = formResponse.answers || raw.answers || [];\n\nconst answerMap = {};\nfor (const answer of answers) {\n  const field =\n    answer.field?.ref ||\n    answer.field?.id ||\n    answer.title ||\n    answer.field?.title ||\n    '';\n  const key = String(field).toLowerCase();\n  let value = '';\n  if (answer.text) value = answer.text;\n  else if (answer.email) value = answer.email;\n  else if (answer.phone_number) value = answer.phone_number;\n  else if (answer.choice?.label) value = answer.choice.label;\n  else if (answer.choices?.labels) value = answer.choices.labels.join(', ');\n  else if (answer.number !== undefined) value = String(answer.number);\n  else if (answer.date) value = answer.date;\n  else if (answer.boolean !== undefined) value = answer.boolean ? 'yes' : 'no';\n  if (key && value) answerMap[key] = value;\n}\n\nconst fromMap = (...aliases) => {\n  for (const alias of aliases) {\n    const hit = answerMap[String(alias).toLowerCase()];\n    if (hit) return hit;\n  }\n  return '';\n};\n\nconst submissionId =\n  raw.submission_id ||\n  raw.submissionID ||\n  formResponse.token ||\n  raw.event_id ||\n  raw.id ||\n  `webhook-${Date.now()}`;\n\nconst receivedAt = raw.submitted_at || raw.created_at || new Date().toISOString();\n\nconst fullName =\n  get('fullName', 'full_name', 'name', 'contact_name') ||\n  fromMap('full_name', 'name', 'contact_name', 'your_name') ||\n  [get('firstName', 'first_name'), get('lastName', 'last_name')].filter(Boolean).join(' ').trim();\n\nconst email =\n  get('email', 'contact_email', 'business_email') ||\n  fromMap('email', 'contact_email', 'business_email', 'work_email');\n\nconst phone =\n  get('phone', 'phone_number', 'mobile', 'contact_phone') ||\n  fromMap('phone', 'phone_number', 'mobile', 'contact_phone', 'cell_phone');\n\nconst businessName =\n  get('businessName', 'business_name', 'company', 'company_name') ||\n  fromMap('business_name', 'company', 'company_name', 'organization');\n\nconst coverageType =\n  get('coverageType', 'coverage_type', 'insurance_type', 'policy_type') ||\n  fromMap('coverage_type', 'insurance_type', 'policy_type', 'what_coverage_do_you_need');\n\nconst industry =\n  get('industry', 'business_type', 'sector') ||\n  fromMap('industry', 'business_type', 'sector', 'what_industry_are_you_in');\n\nconst annualRevenue =\n  get('annualRevenue', 'annual_revenue', 'revenue') ||\n  fromMap('annual_revenue', 'revenue', 'estimated_annual_revenue');\n\nconst employeeCount =\n  get('employeeCount', 'employee_count', 'employees') ||\n  fromMap('employee_count', 'employees', 'number_of_employees');\n\nconst claimsHistory =\n  get('claimsHistory', 'claims_history', 'prior_claims') ||\n  fromMap('claims_history', 'prior_claims', 'claims_in_last_5_years');\n\nconst effectiveDate =\n  get('effectiveDate', 'effective_date', 'coverage_start') ||\n  fromMap('effective_date', 'coverage_start', 'when_do_you_need_coverage');\n\nconst notes =\n  get('notes', 'additional_info', 'message', 'details') ||\n  fromMap('notes', 'additional_info', 'message', 'anything_else_we_should_know');\n\nconst source = get('source', 'lead_source') || fromMap('source', 'lead_source') || settings.defaultLeadSource || 'website_form';\n\nconst quoteHash = `${email}|${phone}|${coverageType}|${businessName}`.toLowerCase();\n\nreturn [\n  {\n    json: {\n      quoteLog: settings.quoteLog,\n      useAirtableLog: settings.useAirtableLog,\n      useSheetsLog: settings.useSheetsLog,\n      airtableBaseId: settings.airtableBaseId,\n      quotesTableName: settings.quotesTableName,\n      googleSheetId: settings.googleSheetId,\n      sheetsTabName: settings.sheetsTabName,\n      notifyChannels: settings.notifyChannels,\n      teamsWebhookUrl: settings.teamsWebhookUrl,\n      slackChannel: settings.slackChannel,\n      twilioFromNumber: settings.twilioFromNumber,\n      agentMobileNumber: settings.agentMobileNumber,\n      hotLeadScoreThreshold: settings.hotLeadScoreThreshold,\n      duplicateWindowHours: settings.duplicateWindowHours,\n      defaultLeadSource: settings.defaultLeadSource,\n      submissionId,\n      receivedAt,\n      fullName,\n      email,\n      phone,\n      businessName,\n      coverageType,\n      industry,\n      annualRevenue,\n      employeeCount,\n      claimsHistory,\n      effectiveDate,\n      notes,\n      source,\n      quoteHash,\n      rawPayload: JSON.stringify(raw).slice(0, 4000),\n      _mockExistingQuotes: Array.isArray(raw._mockExistingQuotes) ? raw._mockExistingQuotes : [],\n      _testScenario: raw._testScenario || '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666640",
      "name": "Check Airtable Log Usage",
      "type": "n8n-nodes-base.if",
      "position": [
        780,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "use-airtable-log",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useAirtableLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666604",
      "name": "Fetch Airtable Quote Log",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1040,
        200
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.airtableBaseId }}"
        },
        "limit": 10,
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.quotesTableName }}"
        },
        "options": {},
        "operation": "search",
        "returnAll": false,
        "filterByFormula": "=OR({Email} = '{{ $json.email }}', {Phone} = '{{ $json.phone }}')"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1,
      "alwaysOutputData": true
    },
    {
      "id": "77777777-7777-7777-8777-666666666641",
      "name": "Create Airtable Lookup Bundle",
      "type": "n8n-nodes-base.code",
      "position": [
        1300,
        200
      ],
      "parameters": {
        "jsCode": "const quote = $('Normalize Quote Data').first().json;\nconst rows = $input\n  .all()\n  .map((item) => item.json)\n  .filter((row) => !row._mockEmpty)\n  .map((row) => row.fields || row);\n\nreturn [{ json: { ...quote, _lookupRows: rows } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666642",
      "name": "Skip Airtable Bundle Creation",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        420
      ],
      "parameters": {
        "jsCode": "const quote = $('Normalize Quote Data').first().json;\nreturn [{ json: { ...quote, _lookupRows: [] } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666643",
      "name": "Check Google Sheets Log Usage",
      "type": "n8n-nodes-base.if",
      "position": [
        1560,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "use-sheets-log",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useSheetsLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666644",
      "name": "Read Quote Log from Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1820,
        200
      ],
      "parameters": {
        "options": {},
        "resource": "sheet",
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.sheetsTabName }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.googleSheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "77777777-7777-7777-8777-666666666645",
      "name": "Combine Sheets Lookup Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        200
      ],
      "parameters": {
        "jsCode": "function fromNode(name) {\n  try {\n    return $(name).first().json;\n  } catch (error) {\n    return null;\n  }\n}\n\nconst prev =\n  fromNode('Create Airtable Lookup Bundle') ||\n  fromNode('Skip Airtable Bundle Creation') ||\n  $('Normalize Quote Data').first().json;\n\nconst sheetRows = $input\n  .all()\n  .map((item) => item.json)\n  .filter((row) => !row._mockEmpty)\n  .map((row) => row.fields || row);\n\nreturn [\n  {\n    json: {\n      ...prev,\n      _lookupRows: [...(prev._lookupRows || []), ...sheetRows],\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666646",
      "name": "Forward Lookup Rows",
      "type": "n8n-nodes-base.code",
      "position": [
        1820,
        420
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: item }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666605",
      "name": "Check for Duplicate Quote",
      "type": "n8n-nodes-base.code",
      "position": [
        2320,
        320
      ],
      "parameters": {
        "jsCode": "const quote = $input.first().json;\nconst rows = Array.isArray(quote._lookupRows) ? quote._lookupRows : [];\nconst windowHours = quote.duplicateWindowHours ?? 24;\nconst cutoff = Date.now() - windowHours * 60 * 60 * 1000;\n\nconst isDuplicate = rows.some((row) => {\n  const record = row.fields || row;\n  if (record._mockEmpty) return false;\n  const existingEmail = (record.Email || record.email || '').toLowerCase();\n  const existingPhone = String(record.Phone || record.phone || '').replace(/\\D/g, '');\n  const existingHash = (record['Quote Hash'] || record.quoteHash || '').toLowerCase();\n  const existingAt = Date.parse(record['Received At'] || record.receivedAt || record.createdTime || '');\n\n  if (quote.email && existingEmail && quote.email.toLowerCase() === existingEmail) {\n    if (!existingAt || existingAt >= cutoff) return true;\n  }\n\n  const phoneDigits = String(quote.phone || '').replace(/\\D/g, '');\n  if (phoneDigits.length >= 10 && existingPhone && phoneDigits === existingPhone) {\n    if (!existingAt || existingAt >= cutoff) return true;\n  }\n\n  if (quote.quoteHash && existingHash && quote.quoteHash === existingHash) {\n    if (!existingAt || existingAt >= cutoff) return true;\n  }\n\n  return false;\n});\n\nreturn [\n  {\n    json: {\n      ...quote,\n      isDuplicate,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666606",
      "name": "Check Quote Novelty",
      "type": "n8n-nodes-base.if",
      "position": [
        2580,
        320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "is-new-quote",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isDuplicate }}",
              "rightValue": false
            },
            {
              "id": "has-contact",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ (($json.email || '').length + ($json.phone || '').length) }}",
              "rightValue": 3
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666607",
      "name": "AI Quote Pre-Qualification",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1560,
        320
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.2
        },
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "You are an insurance agency intake assistant for commercial and personal lines. Return ONLY valid JSON with keys: leadScore (0-100 integer), priority (hot|warm|cold), coverageSummary (string), recommendedProducts (array of strings), underwritingFlags (array of strings), missingInfo (array of strings), nextStep (string), talkingPoints (array of strings). Score higher when coverage is needed soon, revenue/employee count fit target market, contact info is complete, and claims history is favorable. Score lower for incomplete data, high-risk industries without controls, frequent claims, or vague requests. Flag red flags like lapse in coverage, DUI, unfilled roof age, or contractor without GL."
            },
            {
              "role": "user",
              "content": "=Quote request:\nName: {{ $('Check for Duplicate Quote').item.json.fullName }}\nBusiness: {{ $('Check for Duplicate Quote').item.json.businessName }}\nEmail: {{ $('Check for Duplicate Quote').item.json.email }}\nPhone: {{ $('Check for Duplicate Quote').item.json.phone }}\nCoverage requested: {{ $('Check for Duplicate Quote').item.json.coverageType }}\nIndustry: {{ $('Check for Duplicate Quote').item.json.industry }}\nAnnual revenue: {{ $('Check for Duplicate Quote').item.json.annualRevenue }}\nEmployees: {{ $('Check for Duplicate Quote').item.json.employeeCount }}\nClaims history: {{ $('Check for Duplicate Quote').item.json.claimsHistory }}\nEffective date: {{ $('Check for Duplicate Quote').item.json.effectiveDate }}\nSource: {{ $('Check for Duplicate Quote').item.json.source }}\nNotes: {{ $('Check for Duplicate Quote').item.json.notes }}"
            }
          ]
        },
        "resource": "text",
        "operation": "message"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "77777777-7777-7777-8777-666666666608",
      "name": "Analyze AI Quote Response",
      "type": "n8n-nodes-base.code",
      "position": [
        1820,
        320
      ],
      "parameters": {
        "jsCode": "const quote = $('Check for Duplicate Quote').first().json;\nconst aiItem = $input.first().json;\n\nconst raw =\n  aiItem.message?.content ||\n  aiItem.text ||\n  aiItem.output ||\n  aiItem.choices?.[0]?.message?.content ||\n  '';\n\nlet analysis = {\n  leadScore: 50,\n  priority: 'warm',\n  coverageSummary: 'Quote request received',\n  recommendedProducts: [],\n  underwritingFlags: [],\n  missingInfo: [],\n  nextStep: 'Contact prospect within 1 business day',\n  talkingPoints: [],\n};\n\ntry {\n  const jsonMatch = String(raw).match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    analysis = { ...analysis, ...JSON.parse(jsonMatch[0]) };\n  }\n} catch (error) {\n  analysis.coverageSummary = String(raw).slice(0, 500) || analysis.coverageSummary;\n}\n\nconst toList = (value) => {\n  if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);\n  return String(value || '')\n    .split('\\n')\n    .map((item) => item.trim())\n    .filter(Boolean);\n};\n\nanalysis.recommendedProducts = toList(analysis.recommendedProducts);\nanalysis.underwritingFlags = toList(analysis.underwritingFlags);\nanalysis.missingInfo = toList(analysis.missingInfo);\nanalysis.talkingPoints = toList(analysis.talkingPoints);\n\nconst score = Math.max(0, Math.min(100, Number(analysis.leadScore) || 50));\nconst threshold = quote.hotLeadScoreThreshold ?? 75;\n\nlet route = 'warm';\nlet notifyTeams = true;\nlet notifySms = false;\n\nif (score >= threshold || analysis.priority === 'hot') {\n  route = 'hot';\n  notifyTeams = true;\n  notifySms = true;\n} else if (score < 40 || analysis.priority === 'cold') {\n  route = 'cold';\n  notifyTeams = false;\n  notifySms = false;\n}\n\nconst flagsFormatted = analysis.underwritingFlags.join(' | ') || 'None flagged';\nconst productsFormatted = analysis.recommendedProducts.join(', ') || 'General commercial lines';\nconst missingFormatted = analysis.missingInfo.join(' | ') || 'None';\n\nreturn [\n  {\n    json: {\n      ...quote,\n      leadScore: score,\n      priority: analysis.priority || route,\n      coverageSummary: analysis.coverageSummary,\n      recommendedProducts: productsFormatted,\n      underwritingFlags: flagsFormatted,\n      missingInfo: missingFormatted,\n      nextStep: analysis.nextStep,\n      talkingPoints: analysis.talkingPoints.join(' | '),\n      route,\n      notifyTeams,\n      notifySms,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666609",
      "name": "Route by Lead Quality",
      "type": "n8n-nodes-base.switch",
      "position": [
        2080,
        320
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "hot",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "route-hot",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "hot"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "warm",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "route-warm",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "warm"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "cold",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "route-cold",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "cold"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666610",
      "name": "Setup Alert Channels",
      "type": "n8n-nodes-base.code",
      "position": [
        2360,
        240
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst channels = String(item.notifyChannels || 'teams')\n  .toLowerCase()\n  .split(',')\n  .map((value) => value.trim())\n  .filter(Boolean);\n\nconst useTeams = item.notifyTeams && (channels.includes('teams') || channels.includes('both'));\nconst useSlack = item.notifyTeams && (channels.includes('slack') || channels.includes('both'));\n\nconst routeEmoji = {\n  hot: '\ud83d\udd25',\n  warm: '\ud83d\udccb',\n  cold: '\ud83d\udcdd',\n};\n\nconst emoji = routeEmoji[item.route] || '\ud83d\udccb';\nconst headline =\n  item.route === 'hot'\n    ? 'Hot insurance quote lead'\n    : item.route === 'warm'\n      ? 'New insurance quote request'\n      : 'Quote logged (no alert)';\n\nconst details = [\n  `Lead score: ${item.leadScore}/100 (${item.route})`,\n  `Name: ${item.fullName || 'Not provided'}`,\n  `Business: ${item.businessName || 'Not provided'}`,\n  `Email: ${item.email || 'Not provided'}`,\n  `Phone: ${item.phone || 'Not provided'}`,\n  `Coverage requested: ${item.coverageType || 'Not specified'}`,\n  `Industry: ${item.industry || 'Not specified'}`,\n  `Revenue: ${item.annualRevenue || 'Not provided'}`,\n  `Employees: ${item.employeeCount || 'Not provided'}`,\n  `Claims history: ${item.claimsHistory || 'Not provided'}`,\n  `Effective date: ${item.effectiveDate || 'Not provided'}`,\n  `Source: ${item.source}`,\n  `AI summary: ${item.coverageSummary}`,\n  `Recommended products: ${item.recommendedProducts}`,\n  `Underwriting flags: ${item.underwritingFlags}`,\n  `Missing info: ${item.missingInfo}`,\n  `Next step: ${item.nextStep}`,\n  `Talking points: ${item.talkingPoints || 'None'}`,\n  item.notes ? `Notes: ${item.notes}` : '',\n].filter(Boolean);\n\nconst teamsMessage = [\n  `${emoji} **${headline}**`,\n  '',\n  ...details.map((line) => {\n    const [label, ...rest] = line.split(':');\n    return `**${label}:**${rest.join(':')}`;\n  }),\n].join('\\n');\n\nconst slackMessage = [\n  `${emoji} *${headline}*`,\n  '',\n  ...details.map((line) => {\n    const [label, ...rest] = line.split(':');\n    return `*${label}:*${rest.join(':')}`;\n  }),\n].join('\\n');\n\nreturn [\n  {\n    json: {\n      ...item,\n      useTeams,\n      useSlack,\n      teamsMessage,\n      slackMessage,\n      teamsWebhookUrl: item.teamsWebhookUrl,\n      slackChannel: item.slackChannel,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666630",
      "name": "Check Teams Alert Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        2620,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "use-teams",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useTeams }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666611",
      "name": "Send Alert to Microsoft Teams",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2880,
        120
      ],
      "parameters": {
        "url": "={{ $json.teamsWebhookUrl }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ '@type': 'MessageCard', '@context': 'http://schema.org/extensions', themeColor: $json.route === 'hot' ? 'D13438' : '0078D4', summary: 'Insurance quote lead', sections: [{ activityTitle: $json.route === 'hot' ? 'Hot insurance quote lead' : 'New insurance quote request', facts: [ { name: 'Lead score', value: String($json.leadScore) + '/100' }, { name: 'Contact', value: ($json.fullName || 'Unknown') + ' \u2014 ' + ($json.phone || 'no phone') }, { name: 'Business', value: $json.businessName || 'Not provided' }, { name: 'Coverage', value: $json.coverageType || 'Not specified' }, { name: 'Flags', value: $json.underwritingFlags || 'None' } ], text: $json.teamsMessage } ] }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666612",
      "name": "Verify Teams Alert Delivery",
      "type": "n8n-nodes-base.code",
      "position": [
        3140,
        120
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, teamsSent: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666631",
      "name": "Omit Teams Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        2880,
        320
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, teamsSent: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666632",
      "name": "Check Slack Alert Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        3380,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "use-slack",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useSlack }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666633",
      "name": "Send Alert to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        3640,
        120
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.slackChannel }}"
        },
        "operation": "post",
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666634",
      "name": "Verify Slack Alert Delivery",
      "type": "n8n-nodes-base.code",
      "position": [
        3900,
        120
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, slackSent: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666635",
      "name": "Omit Slack Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        3640,
        320
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, slackSent: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666613",
      "name": "Create Cold Log Entry",
      "type": "n8n-nodes-base.code",
      "position": [
        2592,
        520
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, teamsSent: 'no', slackSent: 'no', smsSent: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666614",
      "name": "Check SMS Requirement for Hot Lead",
      "type": "n8n-nodes-base.if",
      "position": [
        4140,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "notify-sms",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.notifySms }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666615",
      "name": "Setup SMS for Hot Lead",
      "type": "n8n-nodes-base.code",
      "position": [
        4432,
        160
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst text = [\n  `HOT LEAD (${item.leadScore}/100)`,\n  item.fullName || 'Unknown contact',\n  item.businessName || 'Unknown business',\n  item.coverageType || 'Coverage TBD',\n  item.phone || 'No phone',\n  `Flags: ${item.underwritingFlags}`,\n].join(' | ');\n\nreturn [\n  {\n    json: {\n      ...item,\n      smsBody: text.slice(0, 320),\n      smsTo: item.agentMobileNumber,\n      smsFrom: item.twilioFromNumber,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666616",
      "name": "Dispatch Twilio SMS Alert",
      "type": "n8n-nodes-base.twilio",
      "position": [
        4688,
        160
      ],
      "parameters": {
        "to": "={{ $json.smsTo }}",
        "from": "={{ $json.smsFrom }}",
        "message": "={{ $json.smsBody }}",
        "resource": "sms",
        "operation": "send"
      },
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "77777777-7777-7777-8777-666666666617",
      "name": "Verify SMS Dispatch",
      "type": "n8n-nodes-base.code",
      "position": [
        4960,
        160
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, smsSent: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666619",
      "name": "Log No SMS Needed",
      "type": "n8n-nodes-base.code",
      "position": [
        4432,
        368
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, smsSent: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666620",
      "name": "Record Quote Log",
      "type": "n8n-nodes-base.code",
      "position": [
        4432,
        736
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nreturn [\n  {\n    json: {\n      quoteLog: item.quoteLog,\n      useAirtableLog: item.useAirtableLog,\n      useSheetsLog: item.useSheetsLog,\n      airtableBaseId: item.airtableBaseId,\n      quotesTableName: item.quotesTableName,\n      googleSheetId: item.googleSheetId,\n      sheetsTabName: item.sheetsTabName,\n      'Received At': item.receivedAt,\n      'Submission ID': item.submissionId,\n      'Quote Hash': item.quoteHash,\n      'Full Name': item.fullName,\n      'Business Name': item.businessName,\n      Email: item.email,\n      Phone: item.phone,\n      'Coverage Type': item.coverageType,\n      Industry: item.industry,\n      'Annual Revenue': item.annualRevenue,\n      Employees: item.employeeCount,\n      'Claims History': item.claimsHistory,\n      'Effective Date': item.effectiveDate,\n      Source: item.source,\n      'Lead Score': item.leadScore,\n      Priority: item.route,\n      'Coverage Summary': item.coverageSummary,\n      'Recommended Products': item.recommendedProducts,\n      'Underwriting Flags': item.underwritingFlags,\n      'Missing Info': item.missingInfo,\n      'Next Step': item.nextStep,\n      'Talking Points': item.talkingPoints,\n      Notes: item.notes,\n      'Teams Sent': item.teamsSent || 'no',\n      'Slack Sent': item.slackSent || 'no',\n      'SMS Sent': item.smsSent || 'no',\n      Status: item.route === 'hot' ? 'Hot - Contact Now' : item.route === 'warm' ? 'Warm - Follow Up' : 'Cold - Nurture',\n      _testScenario: item._testScenario || '',\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666650",
      "name": "Check Airtable Write Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        4704,
        816
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "write-airtable",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useAirtableLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666621",
      "name": "Write Quote to Airtable",
      "type": "n8n-nodes-base.airtable",
      "position": [
        4976,
        704
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.airtableBaseId }}"
        },
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.quotesTableName }}"
        },
        "columns": {
          "value": {
            "Email": "={{ $json.Email }}",
            "Notes": "={{ $json.Notes }}",
            "Phone": "={{ $json.Phone }}",
            "Source": "={{ $json.Source }}",
            "Status": "={{ $json.Status }}",
            "Industry": "={{ $json.Industry }}",
            "Priority": "={{ $json.Priority }}",
            "SMS Sent": "={{ $json['SMS Sent'] }}",
            "Employees": "={{ $json.Employees }}",
            "Full Name": "={{ $json['Full Name'] }}",
            "Next Step": "={{ $json['Next Step'] }}",
            "Lead Score": "={{ $json['Lead Score'] }}",
            "Quote Hash": "={{ $json['Quote Hash'] }}",
            "Slack Sent": "={{ $json['Slack Sent'] }}",
            "Teams Sent": "={{ $json['Teams Sent'] }}",
            "Received At": "={{ $json['Received At'] }}",
            "Missing Info": "={{ $json['Missing Info'] }}",
            "Business Name": "={{ $json['Business Name'] }}",
            "Coverage Type": "={{ $json['Coverage Type'] }}",
            "Submission ID": "={{ $json['Submission ID'] }}",
            "Annual Revenue": "={{ $json['Annual Revenue'] }}",
            "Claims History": "={{ $json['Claims History'] }}",
            "Effective Date": "={{ $json['Effective Date'] }}",
            "Talking Points": "={{ $json['Talking Points'] }}",
            "Coverage Summary": "={{ $json['Coverage Summary'] }}",
            "Underwriting Flags": "={{ $json['Underwriting Flags'] }}",
            "Recommended Products": "={{ $json['Recommended Products'] }}"
          },
          "schema": [
            {
              "id": "Received At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Received At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Submission ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Submission ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Quote Hash",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Quote Hash",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Full Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Full Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Business Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Business Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Email",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Coverage Type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Coverage Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Industry",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Industry",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Annual Revenue",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Annual Revenue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Employees",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Employees",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Claims History",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Claims History",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Effective Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Effective Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Lead Score",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Lead Score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Priority",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Priority",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Coverage Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Coverage Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Recommended Products",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Recommended Products",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Underwriting Flags",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Underwriting Flags",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Missing Info",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Missing Info",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Next Step",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Next Step",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Talking Points",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Talking Points",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Teams Sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Teams Sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Slack Sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Slack Sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SMS Sent",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SMS Sent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "77777777-7777-7777-8777-666666666651",
      "name": "Verify Airtable Log Write",
      "type": "n8n-nodes-base.code",
      "position": [
        5232,
        704
      ],
      "parameters": {
        "jsCode": "const prior = $('Record Quote Log').first().json;\nconst item = $input.first().json;\nreturn [{ json: { ...prior, airtableLogged: 'yes', airtableMocked: false, airtableRecordId: item.id || `recLocal${Date.now()}` } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666652",
      "name": "Omit Airtable Write",
      "type": "n8n-nodes-base.code",
      "position": [
        4976,
        896
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nreturn [{ json: { ...item, airtableLogged: 'no' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "77777777-7777-7777-8777-666666666653",
      "name": "Check Sheets Write Requirement",
      "type": "n8n-nodes-base.if",
      "position": [
        5456,
        560
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "write-sheets",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.useSheetsLog }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "77777777-7777-7777-8777-666666666654",
      "name": "Add Quote to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5712,
        448
      ],
      "parameters": {
        "columns": {
          "value": {
            "Email": "={{ $json.Email }}",
            "Notes": "={{ $json.Notes }}",
            "Phone": "={{ $json.Phone }}",
            "Source": "={{ $json.Source }}",
            "Status": "={{ $json.Status }}",
            "Industry": "={{ $json.Industry }}",
            "Priority": "={{ $json.Priority }}",
            "SMS Sent": "={{ $json['SMS Sent'] }}",
            "Employees": "={{ $json.Employees }}",
            "Full Name": "={{ $json['Full Name'] }}",
            "Next Step": "={{ $json['Next Step'] }}",
            "Lead Score": "={{ $json['Lead Score'] }}",
            "Quote Hash": "={{ $json['Quote Hash'] }}",
            "Slack Sent": "={{ $json['Slack Sent'] }}",
            "Teams Sent": "={{ $json['Teams Sent'] }}",
            "Received At": "={{ $json['Received At'] }}",
            "Missing Info": "={{ $json['Missing Info'] }}",
            "Business Name": "={{ $json['Business Name'] }}",
            "Coverage Type": "={{ $json['Coverage Type'] }}",
            "Submission ID": "={{ $json['Submission ID'] }}",
            "Annual Revenue": "={{ $json['Annual Revenue'] }}",
            "Claims History": "={{ $json['Claims History'] }}",
            "Effective Date": "={{ $json['Effective Date'] }}",
            "Talking Points": "={{ $json['Talking Points'] }}",
            "Coverage Summary": "={{ $json['Coverage Summary'] }}",
            "Underwriting Flags": "={{ $json['Underwriting Flags'] }}",
            "Recommended Products": "={{ $json['Recommended Products'] }}"
          },
          "schema": [
            {
              "id": "Received At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Received At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Submission ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Submission ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Quote Hash",
              "type": "string",
              "display": tr

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

This workflow receives insurance quote submissions via a webhook, normalizes and deduplicates them against Airtable and/or Google Sheets, uses OpenAI to score and classify the lead, then routes hot/warm leads to Microsoft Teams and/or Slack (plus Twilio SMS for hot leads) and…

Source: https://n8n.io/workflows/17075/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This workflow monitors Gmail for messages labeled “Admin”, redacts sensitive data, uses OpenAI to extract structured admin tasks and reminders, skips duplicates by checking Google Sheets and/or Airtab

Gmail Trigger, Airtable, Google Sheets +3
Data & Sheets

Splitout Code. Uses httpRequest, stickyNote, manualTrigger, splitOut. Event-driven trigger; 33 nodes.

HTTP Request, OpenAI, Noco Db +3
Data & Sheets

This workflow automates the process of gathering LinkedIn advice articles, extracting their content, and generating unique contributions for each article using an AI model. The contributions are then

HTTP Request, OpenAI, Noco Db +3
Data & Sheets

2491. Uses httpRequest, openAi, nocoDb, slack. Event-driven trigger; 33 nodes.

HTTP Request, OpenAI, Noco Db +3
Data & Sheets

01 — Lead Capture → AI Scoring → CRM Pipeline. Uses openAi, gmail, airtable, slack. Webhook trigger; 20 nodes.

OpenAI, Gmail, Airtable +3