{
  "name": "Find Oregon contractors with insurance expiring soon and log renewal leads",
  "nodes": [
    {
      "parameters": {
        "content": "### How it works\nThis workflow helps commercial insurance agents and surety producers in Oregon find contractors whose liability insurance is coming up for renewal. Each morning it pulls active Oregon CCB licenses through the Loomwise Oregon Contractor Licenses Actor, keeps rows whose insurance expiration date falls in the next N days (60 by default), and compares license numbers against your Google Sheet so each contractor is logged once. Already-expired policies are dropped. A digest can email or post the new count and the soonest expirations. Failures after retries send an alert.\n\nThe Actor returns the newest licenses first and does not filter by insurance date. To reach older active contractors, set registeredWithinDays to 0 and raise maxResults (you pay per result). Start with 200-500 and increase if your counties are large.\n\n### Setup\n1. Install `@apify/n8n-nodes-apify` (v0.6+; https://docs.apify.com/integrations/n8n) and connect your Apify API credential. Never paste an API token into a node.\n2. Open https://apify.com/loomwise/oregon-contractor-licenses.\n3. Create a Google Sheet with header row: uniqueId, licenseNumber, businessName, licenseTypeName, city, county, businessPhone, businessAddress, insuranceCompany, insuranceExpirationDate, bondCompany, retrievedAt.\n4. Connect Google Sheets OAuth. In Configure filters, paste the spreadsheet ID and set counties, optional cities, license types, keywords, daysUntilInsuranceExpiry, registeredWithinDays, and maxResults.\n5. Connect Gmail or Slack and replace the placeholders. Run once manually, then activate. Cost is about $3 per 1,000 licenses. More data tools: https://loomwise.app/data/\n\n### Customization\nNarrow to RGC or CGC license types, raise the expiry window, or add name keywords such as roof or solar. Send the digest to a producer Slack channel if that is where the team works.\n",
        "height": 740,
        "width": 520,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -100
      ],
      "id": "893d8da9-bfac-415a-8225-b976814e27af",
      "name": "Overview"
    },
    {
      "parameters": {
        "content": "## 1. Schedule and configuration\nSet filters, the Google Sheet ID, and notification choices here. Later nodes reuse these values.",
        "height": 200,
        "width": 480,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        20,
        -100
      ],
      "id": "936e5588-692c-408f-b4e9-7fe7521114f9",
      "name": "## 1. Schedule and configuration"
    },
    {
      "parameters": {
        "content": "## 2. Load existing rows and run Apify\nRead unique IDs already in the sheet, then run the Actor with your filters. The Apify node uses your API credential, not a pasted token.",
        "height": 200,
        "width": 720,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        540,
        -100
      ],
      "id": "884733e6-f9ee-4713-a688-4e465d525486",
      "name": "## 2. Load existing rows and run Apify"
    },
    {
      "parameters": {
        "content": "## 3. Filter and deduplicate\nDrop rows that fail your extra rules or already exist in the sheet, then build a short summary of what is new.",
        "height": 200,
        "width": 460,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1300,
        -100
      ],
      "id": "08544ccc-1435-46f1-a725-6687f6665d70",
      "name": "## 3. Filter and deduplicate"
    },
    {
      "parameters": {
        "content": "## 4. Save new rows and notify\nAppend only new rows. If notifications are on, send the count and top items by Gmail or Slack.",
        "height": 200,
        "width": 720,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1800,
        -100
      ],
      "id": "c69c4d49-5edf-49df-aff3-18d9bfd70e62",
      "name": "## 4. Save new rows and notify"
    },
    {
      "parameters": {
        "content": "## 5. Handle failures\nIf the sheet read, Apify run, or append fails after retries, skip writing from that step and send a failure alert on your chosen channel.",
        "height": 160,
        "width": 720,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        540,
        520
      ],
      "id": "d5143344-e24c-4503-9e04-234879a2a28e",
      "name": "## 5. Handle failures"
    },
    {
      "parameters": {
        "content": "Replace every REPLACE_WITH_\u2026 value in Configure filters, then connect Apify, Google Sheets, and Gmail or Slack credentials before you run or activate this workflow.",
        "height": 200,
        "width": 480,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        20,
        520
      ],
      "id": "c7deae25-1dcc-4b78-aa6b-c2e3ef84f939",
      "name": "\u26a0 Replace placeholders before you run"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 7,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        40,
        220
      ],
      "id": "f2f459b9-e992-4d09-a336-19f980d73c3c",
      "name": "Every day 7am"
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "957bcc20-3951-4c0a-a08b-ade5f381e73f",
              "name": "workflowTitle",
              "value": "Oregon contractor insurance renewals",
              "type": "string"
            },
            {
              "id": "1b57b500-0bf2-4fdc-aa6b-c716df5023e8",
              "name": "spreadsheetId",
              "value": "REPLACE_WITH_YOUR_GOOGLE_SHEET_ID",
              "type": "string"
            },
            {
              "id": "f8076994-74a4-4dd7-aab0-ff3afb0cdf4d",
              "name": "sheetName",
              "value": "Sheet1",
              "type": "string"
            },
            {
              "id": "207561dc-fed1-4129-ac54-b8b6aaa5286e",
              "name": "uniqueIdField",
              "value": "licenseNumber",
              "type": "string"
            },
            {
              "id": "84476787-4e80-458d-8776-f69906e2ebcc",
              "name": "registeredWithinDays",
              "value": 0,
              "type": "number"
            },
            {
              "id": "363e2619-9927-44b9-90ed-b4db24896e4d",
              "name": "countiesCsv",
              "value": "MULTNOMAH,WASHINGTON,CLACKAMAS",
              "type": "string"
            },
            {
              "id": "92b60d00-2cd5-40f8-969e-b8bce0872b52",
              "name": "citiesCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "8b3b0c10-7506-4d73-9c5e-e5424eef2062",
              "name": "licenseTypesCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "29b907de-e7fa-4890-ae2a-648910b2bae4",
              "name": "keywordsCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "4679f866-ab97-4be2-8f38-770b5e368ade",
              "name": "daysUntilInsuranceExpiry",
              "value": 60,
              "type": "number"
            },
            {
              "id": "b94883e6-7d46-4039-9cae-2fbbe6de5650",
              "name": "maxResults",
              "value": 200,
              "type": "number"
            },
            {
              "id": "6dec7411-e9b3-4aff-8c7a-c2a4642302f3",
              "name": "sendSummaryNotification",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "110730bf-74cc-4fe8-9772-e1576415b550",
              "name": "notificationChannel",
              "value": "gmail",
              "type": "string"
            },
            {
              "id": "bcea7ab6-a82f-4707-9820-018b65f8853d",
              "name": "notifyEmail",
              "value": "REPLACE_WITH_YOUR_EMAIL@example.com",
              "type": "string"
            },
            {
              "id": "aa5e3de3-8e75-4b52-a4d8-89670e182e32",
              "name": "slackChannelId",
              "value": "REPLACE_WITH_YOUR_SLACK_CHANNEL_ID",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        220
      ],
      "id": "e419841f-10d2-4cd8-9776-729d078919c9",
      "name": "Configure filters"
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Configure filters').first().json.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Configure filters').first().json.sheetName }}",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        540,
        220
      ],
      "id": "6959dea1-65f6-4cf6-a833-73ec4129bafb",
      "name": "Read existing sheet rows",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_YOUR_GOOGLE_SHEETS_CREDENTIAL_ID",
          "name": "Google Sheets account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Configure filters').first().json;\nconst field = String(cfg.uniqueIdField || '').trim();\nconst existingIds = [];\nconst seen = new Set();\n\nfunction computeId(row) {\n  if (!row || typeof row !== 'object') return '';\n  const stored = String(row.uniqueId || '').trim();\n  if (stored && stored.toLowerCase() !== 'uniqueid') return stored;\n  if (field === 'cityId+permitNumber') {\n    return [row.cityId || row.city, row.permitNumber].map((v) => String(v || '').trim()).filter(Boolean).join('|');\n  }\n  if (field === 'taxpayerNumber+outletNumber') {\n    return [row.taxpayerNumber, row.outletNumber].map((v) => String(v || '').trim()).filter(Boolean).join('|');\n  }\n  return String(row[field] || '').trim();\n}\n\nfor (const item of $input.all()) {\n  const id = computeId(item.json || {});\n  if (id && !seen.has(id)) {\n    seen.add(id);\n    existingIds.push(id);\n  }\n}\n\nreturn [{\n  json: {\n    ...cfg,\n    existingIds,\n    existingCount: existingIds.length,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        780,
        220
      ],
      "id": "d97bfc5f-7708-46cf-9004-12bb8d157808",
      "name": "Collect existing IDs"
    },
    {
      "parameters": {
        "operation": "Run actor and get dataset",
        "actorId": "loomwise~oregon-contractor-licenses",
        "customBody": "={{ JSON.stringify({ registeredWithinDays: $json.registeredWithinDays, counties: String($json.countiesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), cities: String($json.citiesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), keywords: String($json.keywordsCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), maxResults: $json.maxResults, includeContactDetails: true, includeInsuranceBond: true }) }}",
        "timeout": null,
        "memory": 1024,
        "resource": "Actors",
        "actorInputMode": "json",
        "authentication": "apifyApi"
      },
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        1040,
        220
      ],
      "id": "4171bd17-cfac-47ea-9f78-7279c18d6979",
      "name": "Fetch OR CCB licenses",
      "credentials": {
        "apifyApi": {
          "id": "REPLACE_WITH_YOUR_APIFY_CREDENTIAL_ID",
          "name": "Apify account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 10000,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Collect existing IDs').first().json;\nconst existing = new Set((cfg.existingIds || []).map((id) => String(id)));\nconst field = String(cfg.uniqueIdField || '').trim();\nconst channel = String(cfg.notificationChannel || 'none').trim().toLowerCase();\nconst sendSummary = cfg.sendSummaryNotification === true || String(cfg.sendSummaryNotification).toLowerCase() === 'true';\n\nfunction computeId(row) {\n  if (!row || typeof row !== 'object') return '';\n  if (field === 'cityId+permitNumber') {\n    return [row.cityId || row.city, row.permitNumber].map((v) => String(v || '').trim()).filter(Boolean).join('|');\n  }\n  if (field === 'taxpayerNumber+outletNumber') {\n    return [row.taxpayerNumber, row.outletNumber].map((v) => String(v || '').trim()).filter(Boolean).join('|');\n  }\n  return String(row[field] || '').trim();\n}\n\nfunction hasActorFields(row) {\n  return Boolean(computeId(row));\n}\n\nfunction csvList(value) {\n  return String(value || '').split(',').map((s) => s.trim()).filter(Boolean);\n}\nfunction parseDay(value) {\n  if (!value) return null;\n  const d = new Date(value);\n  return Number.isNaN(d.getTime()) ? null : d;\n}\nfunction passesExtraFilters(row, cfg) {\n  const types = csvList(cfg.licenseTypesCsv).map((s) => s.toLowerCase());\n  if (types.length) {\n    const hay = `${row.licenseType || ''} ${row.licenseTypeName || ''}`.toLowerCase();\n    if (!types.some((t) => hay.includes(t))) return false;\n  }\n  const keywords = csvList(cfg.keywordsCsv).map((s) => s.toLowerCase());\n  if (keywords.length) {\n    const hay = `${row.businessName || ''}`.toLowerCase();\n    if (!keywords.some((k) => hay.includes(k))) return false;\n  }\n  const windowDays = Number(cfg.daysUntilInsuranceExpiry);\n  const now = new Date();\n  now.setHours(0, 0, 0, 0);\n  const exp = parseDay(row.insuranceExpirationDate);\n  if (!exp) return false;\n  if (exp < now) return false;\n  if (Number.isFinite(windowDays) && windowDays >= 0) {\n    const cutoff = new Date(now.getTime() + windowDays * 86400000);\n    if (exp > cutoff) return false;\n  }\n  return true;\n}\nfunction mapRow(row, uniqueId) {\n  return {\n    uniqueId,\n    licenseNumber: row.licenseNumber || '',\n    businessName: row.businessName || '',\n    licenseTypeName: row.licenseTypeName || row.licenseType || '',\n    city: row.city || '',\n    county: row.county || '',\n    businessPhone: row.businessPhone || '',\n    businessAddress: row.businessAddress || '',\n    insuranceCompany: row.insuranceCompany || '',\n    insuranceExpirationDate: row.insuranceExpirationDate || '',\n    bondCompany: row.bondCompany || '',\n    retrievedAt: row.retrievedAt || new Date().toISOString(),\n  };\n}\n\nconst fetched = [];\nfor (const item of $input.all()) {\n  const row = item.json || {};\n  if (hasActorFields(row)) fetched.push(row);\n}\n\nlet filteredOut = 0;\nlet skippedDuplicates = 0;\nconst newRows = [];\nconst seenThisRun = new Set();\n\nfor (const row of fetched) {\n  if (!passesExtraFilters(row, cfg)) {\n    filteredOut += 1;\n    continue;\n  }\n  const uniqueId = computeId(row);\n  if (!uniqueId) {\n    filteredOut += 1;\n    continue;\n  }\n  if (existing.has(uniqueId) || seenThisRun.has(uniqueId)) {\n    skippedDuplicates += 1;\n    continue;\n  }\n  seenThisRun.add(uniqueId);\n  const mapped = mapRow(row, uniqueId);\n  newRows.push(mapped);\n}\n\nconst topItems = newRows.slice(0, 8);\nconst lines = topItems.map((row, idx) => `${idx + 1}. ${row.businessName} (${row.licenseNumber}) in ${row.city || 'n/a'} \u2014 insurance expires ${row.insuranceExpirationDate || 'n/a'} (${row.insuranceCompany || 'n/a'})`);\nconst intro = `Oregon CCB contractors with insurance expiring in your window, newly added to the sheet: ${newRows.length}.`;\nconst body = newRows.length\n  ? [intro, '', ...lines, '', `Skipped ${skippedDuplicates} already in the sheet. Filtered out ${filteredOut} extra rows. Existing IDs loaded: ${cfg.existingCount || 0}.`].join('\\n')\n  : \"No Oregon contractors in this pull have insurance expiring in your window (or they were already in the sheet).\";\nconst subject = `OR insurance renewals: ${newRows.length} contractor(s)`;\nconst slackText = body;\n\nreturn [{\n  json: {\n    fetchedCount: fetched.length,\n    newCount: newRows.length,\n    skippedDuplicates,\n    filteredOut,\n    existingCount: cfg.existingCount || 0,\n    newRows,\n    topItems,\n    sendSummaryNotification: sendSummary,\n    notificationChannel: channel,\n    notifyEmail: cfg.notifyEmail,\n    slackChannelId: cfg.slackChannelId,\n    subject,\n    body,\n    slackText,\n  },\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        220
      ],
      "id": "7433440e-236f-4393-b178-2e8b7a6f35b5",
      "name": "Keep only new matching leads"
    },
    {
      "parameters": {
        "fieldToSplitOut": "newRows",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1560,
        140
      ],
      "id": "48139168-2135-433e-976c-f3f8e7946da3",
      "name": "Split out new rows"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Configure filters').first().json.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Configure filters').first().json.sheetName }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "uniqueId": "={{ $json.uniqueId }}",
            "licenseNumber": "={{ $json.licenseNumber }}",
            "businessName": "={{ $json.businessName }}",
            "licenseTypeName": "={{ $json.licenseTypeName }}",
            "city": "={{ $json.city }}",
            "county": "={{ $json.county }}",
            "businessPhone": "={{ $json.businessPhone }}",
            "businessAddress": "={{ $json.businessAddress }}",
            "insuranceCompany": "={{ $json.insuranceCompany }}",
            "insuranceExpirationDate": "={{ $json.insuranceExpirationDate }}",
            "bondCompany": "={{ $json.bondCompany }}",
            "retrievedAt": "={{ $json.retrievedAt }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "uniqueId",
              "displayName": "uniqueId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "licenseNumber",
              "displayName": "licenseNumber",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "businessName",
              "displayName": "businessName",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "licenseTypeName",
              "displayName": "licenseTypeName",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "city",
              "displayName": "city",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "county",
              "displayName": "county",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "businessPhone",
              "displayName": "businessPhone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "businessAddress",
              "displayName": "businessAddress",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "insuranceCompany",
              "displayName": "insuranceCompany",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "insuranceExpirationDate",
              "displayName": "insuranceExpirationDate",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "bondCompany",
              "displayName": "bondCompany",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "retrievedAt",
              "displayName": "retrievedAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1840,
        140
      ],
      "id": "83903ec7-45fa-4ea1-833c-65cfaea9cb2d",
      "name": "Append new rows to Sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_YOUR_GOOGLE_SHEETS_CREDENTIAL_ID",
          "name": "Google Sheets account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "95c3df2b-211f-4e25-9d27-cda9702a107d",
              "leftValue": "={{ $json.sendSummaryNotification }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1560,
        340
      ],
      "id": "82078be7-5855-4e66-95f2-472bc2b1bfee",
      "name": "Should send a summary?"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "d846b0db-b371-494d-8d0e-91023a642675",
                    "leftValue": "={{ $json.notificationChannel }}",
                    "rightValue": "gmail",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "gmail"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "8398cbd5-669b-47b7-b978-375da274d070",
                    "leftValue": "={{ $json.notificationChannel }}",
                    "rightValue": "slack",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "slack"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1840,
        340
      ],
      "id": "1306b6f5-5af6-4e2c-8426-76985dd77c66",
      "name": "Route the notification"
    },
    {
      "parameters": {
        "sendTo": "={{ $json.notifyEmail }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2120,
        260
      ],
      "id": "97e3e94a-8478-43e5-b6ad-fa8a916a2672",
      "name": "Email the run summary",
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_WITH_YOUR_GMAIL_CREDENTIAL_ID",
          "name": "Gmail account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "={{ $json.slackChannelId }}",
          "mode": "id"
        },
        "text": "={{ $json.slackText }}",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        2120,
        420
      ],
      "id": "c78b0000-96b0-4723-abb3-5a285db76f06",
      "name": "Post the run summary to Slack",
      "credentials": {
        "slackApi": {
          "id": "REPLACE_WITH_YOUR_SLACK_CREDENTIAL_ID",
          "name": "Slack account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Configure filters').first().json;\nconst item = $input.first() || {};\nconst payload = item.json || {};\nconst err = item.error || payload.error || {};\nconst message = err.message || payload.message || payload.errorMessage || 'Unknown error (open the failed execution in n8n for details).';\nconst channel = String(cfg.notificationChannel || 'none').trim().toLowerCase();\nconst title = cfg.workflowTitle || 'Loomwise lead workflow';\nconst subject = `Workflow failed: ${title}`;\nconst body = [\n  `The n8n workflow \"${title}\" hit an error and stopped.`,\n  'New rows were not appended from this failed step. Open the failed execution in n8n, fix the issue, then run it again.',\n  '',\n  `Error: ${message}`,\n].join('\\n');\n\nreturn [{\n  json: {\n    isFailure: true,\n    notificationChannel: channel,\n    notifyEmail: cfg.notifyEmail,\n    slackChannelId: cfg.slackChannelId,\n    subject,\n    body,\n    slackText: body,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        720
      ],
      "id": "5249b525-3d7c-4edf-9b57-74ca06130942",
      "name": "Format a run error"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "4a3961e9-57c5-473b-8654-d3aa45a472e5",
                    "leftValue": "={{ $json.notificationChannel }}",
                    "rightValue": "gmail",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "gmail"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "f243d6c2-a3a1-4d0c-990a-9daa8c3a8c92",
                    "leftValue": "={{ $json.notificationChannel }}",
                    "rightValue": "slack",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "slack"
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1320,
        720
      ],
      "id": "2f26ac56-ac66-47a9-9c71-cac2c2f20b28",
      "name": "Route the failure alert"
    },
    {
      "parameters": {
        "sendTo": "={{ $json.notifyEmail }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.body }}",
        "options": {}
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1600,
        640
      ],
      "id": "9d210245-eb54-49e8-ba56-30920ac96356",
      "name": "Email a failure alert",
      "credentials": {
        "gmailOAuth2": {
          "id": "REPLACE_WITH_YOUR_GMAIL_CREDENTIAL_ID",
          "name": "Gmail account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "={{ $json.slackChannelId }}",
          "mode": "id"
        },
        "text": "={{ $json.slackText }}",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1600,
        820
      ],
      "id": "2222fa58-468e-4e42-91e4-5fbb609c0b9e",
      "name": "Post a failure alert to Slack",
      "credentials": {
        "slackApi": {
          "id": "REPLACE_WITH_YOUR_SLACK_CREDENTIAL_ID",
          "name": "Slack account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    }
  ],
  "pinData": {},
  "connections": {
    "Every day 7am": {
      "main": [
        [
          {
            "node": "Configure filters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure filters": {
      "main": [
        [
          {
            "node": "Read existing sheet rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read existing sheet rows": {
      "main": [
        [
          {
            "node": "Collect existing IDs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format a run error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect existing IDs": {
      "main": [
        [
          {
            "node": "Fetch OR CCB licenses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch OR CCB licenses": {
      "main": [
        [
          {
            "node": "Keep only new matching leads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format a run error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep only new matching leads": {
      "main": [
        [
          {
            "node": "Split out new rows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Should send a summary?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split out new rows": {
      "main": [
        [
          {
            "node": "Append new rows to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append new rows to Sheet": {
      "main": [
        [],
        [
          {
            "node": "Format a run error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should send a summary?": {
      "main": [
        [
          {
            "node": "Route the notification",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Route the notification": {
      "main": [
        [
          {
            "node": "Email the run summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post the run summary to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format a run error": {
      "main": [
        [
          {
            "node": "Route the failure alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route the failure alert": {
      "main": [
        [
          {
            "node": "Email a failure alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post a failure alert to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "callerPolicy": "workflowsFromSameOwner"
  },
  "versionId": "e18d1430-2a7c-49fa-87a6-04bcb9fc7d33",
  "meta": {
    "templateCredsSetupCompleted": false,
    "instanceId": "loomwise-template-export"
  },
  "tags": []
}
