{
  "name": "Deduplicate weekly Texas restaurant permits into Google Sheets with a digest",
  "nodes": [
    {
      "parameters": {
        "content": "### How it works\nThis workflow helps merchant services, POS vendors, restaurant insurers, and local agencies in Texas find newly permitted restaurants. Every Monday it runs the Loomwise Texas New Business Leads Actor filtered to your cities and NAICS prefixes (722, Food Services, by default), then matches each location on taxpayer number plus outlet number against your Google Sheet. Only new permits are appended. You can also keep name keywords or ZIP prefixes in the configuration node. When the run finishes, it can email or post a digest with the new count and top locations. Failures after retries send an alert instead of writing duplicate or partial rows.\n\n### Setup\n1. Install `@apify/n8n-nodes-apify` (Settings \u2192 Community Nodes, v0.6+; https://docs.apify.com/integrations/n8n).\n2. Connect your Apify API credential. Do not paste an API token into the workflow.\n3. Create a Google Sheet with header row: uniqueId, taxpayerNumber, outletNumber, businessName, locationName, permitIssueDate, naicsCode, organizationType, city, zipCode, locationAddress, retrievedAt.\n4. Connect Google Sheets OAuth. In Configure filters, paste the spreadsheet ID and set cities, NAICS prefixes, optional keywords or ZIP prefixes, and maxResults (100 for a first run).\n5. Connect Gmail or Slack, set notificationChannel, and replace the placeholders. Set sendSummaryNotification to false to skip the digest.\n6. Run once manually, then activate. The Actor is pay-per-result (about $2 per 1,000). Texas refreshes the public file about weekly, so a Monday schedule fits. Actor: https://apify.com/loomwise/texas-new-business-leads \u2014 more data tools: https://loomwise.app/data/\n\n### Customization\nSwitch NAICS to 238 (specialty trades) or 812 (personal services), narrow to one city, or add ZIP prefixes such as 770 for Houston. Send the digest to Slack if that is where the sales team works.\n",
        "height": 740,
        "width": 520,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -100
      ],
      "id": "6059b1bb-563c-4f52-ba20-78d3cc19acfa",
      "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": "71319d73-a8cf-4515-a104-6ef3336fb8b5",
      "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": "7949f028-985c-4049-9834-13ea445d32a6",
      "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": "501f54e3-c06f-43a9-917a-59fc8e55d79e",
      "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": "b643e95c-0821-4ee7-8eaf-753c8996e7fb",
      "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": "477faff9-6c06-46c4-b47b-0db767ca1979",
      "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": "a2871e59-74eb-4cd2-8bcf-1aae4c768c9d",
      "name": "\u26a0 Replace placeholders before you run"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 7,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        40,
        220
      ],
      "id": "05e0fffa-3862-4a90-bb99-4b3b53881d36",
      "name": "Every Monday 7am"
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "38b217ec-0ac1-4523-a8ef-b2ed4872b554",
              "name": "workflowTitle",
              "value": "Weekly Texas restaurant permits",
              "type": "string"
            },
            {
              "id": "df6d9b1a-d5fb-4025-be90-05f7c050dbb5",
              "name": "spreadsheetId",
              "value": "REPLACE_WITH_YOUR_GOOGLE_SHEET_ID",
              "type": "string"
            },
            {
              "id": "377054b6-222e-405f-a4b8-eef9d630f7d1",
              "name": "sheetName",
              "value": "Sheet1",
              "type": "string"
            },
            {
              "id": "041bce51-8c93-4627-822d-320fbe643894",
              "name": "uniqueIdField",
              "value": "taxpayerNumber+outletNumber",
              "type": "string"
            },
            {
              "id": "7f3a4ce1-2066-4dd9-ab91-96774db05ce2",
              "name": "issuedWithinDays",
              "value": 8,
              "type": "number"
            },
            {
              "id": "f6b70afe-07c4-491a-9120-8bbb6f4c3d2d",
              "name": "citiesCsv",
              "value": "HOUSTON,DALLAS,AUSTIN",
              "type": "string"
            },
            {
              "id": "e2c42299-8739-421e-974e-216e8f2da830",
              "name": "naicsPrefixesCsv",
              "value": "722",
              "type": "string"
            },
            {
              "id": "d7c44876-70b7-479d-bd2f-e6a5167ca809",
              "name": "keywordsCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "3e8c9a7a-5546-4aed-963c-b7b7fab6c5b9",
              "name": "zipPrefixesCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "0a6d9d46-8e7d-47ba-9db5-8ba234bc0a51",
              "name": "maxResults",
              "value": 100,
              "type": "number"
            },
            {
              "id": "c29fe260-4f93-4a1c-b20b-934d4af26ffe",
              "name": "sendSummaryNotification",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "0b30b4fb-71a0-4545-b8ac-f1bc02d13c54",
              "name": "notificationChannel",
              "value": "gmail",
              "type": "string"
            },
            {
              "id": "57702a4a-bb1e-41ee-adad-7eb55ecf363b",
              "name": "notifyEmail",
              "value": "REPLACE_WITH_YOUR_EMAIL@example.com",
              "type": "string"
            },
            {
              "id": "8a8274ce-0d8e-48e4-8203-bdf78a9f917f",
              "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": "c379a788-6cde-44d3-a03d-349176db9c51",
      "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": "d9a26eb4-5d07-4f55-ac05-d51648625ee4",
      "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": "484a37b1-23f7-4c0c-9f4c-e0dedad498f7",
      "name": "Collect existing IDs"
    },
    {
      "parameters": {
        "operation": "Run actor and get dataset",
        "actorId": "loomwise~texas-new-business-leads",
        "customBody": "={{ JSON.stringify({ issuedWithinDays: $json.issuedWithinDays, cities: String($json.citiesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), naicsPrefixes: String($json.naicsPrefixesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), keywords: String($json.keywordsCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), zipCodes: String($json.zipPrefixesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), maxResults: $json.maxResults, includeAddress: true }) }}",
        "timeout": null,
        "memory": 1024,
        "resource": "Actors",
        "actorInputMode": "json",
        "authentication": "apifyApi"
      },
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        1040,
        220
      ],
      "id": "a60a6643-d27c-4545-b7ea-c3c9205d836d",
      "name": "Fetch TX new restaurant permits",
      "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 passesExtraFilters(row, cfg) {\n  const keywords = csvList(cfg.keywordsCsv).map((s) => s.toLowerCase());\n  if (keywords.length) {\n    const hay = `${row.businessName || ''} ${row.locationName || ''}`.toLowerCase();\n    if (!keywords.some((k) => hay.includes(k))) return false;\n  }\n  const zips = csvList(cfg.zipPrefixesCsv);\n  if (zips.length) {\n    const zip = String(row.zipCode || '');\n    if (!zips.some((p) => zip.startsWith(p))) return false;\n  }\n  return true;\n}\nfunction mapRow(row, uniqueId) {\n  return {\n    uniqueId,\n    taxpayerNumber: row.taxpayerNumber || '',\n    outletNumber: row.outletNumber || '',\n    businessName: row.businessName || '',\n    locationName: row.locationName || '',\n    permitIssueDate: row.permitIssueDate || '',\n    naicsCode: row.naicsCode || '',\n    organizationType: row.organizationType || '',\n    city: row.city || '',\n    zipCode: row.zipCode || '',\n    locationAddress: row.locationAddress || '',\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} \u2014 ${row.city || 'n/a'} (${row.naicsCode || 'n/a'}) issued ${row.permitIssueDate || 'n/a'}`);\nconst intro = `New Texas food-service sales-tax permits appended this week: ${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 new Texas restaurant permits matched your filters (or they were already in the sheet).\";\nconst subject = `Texas restaurant leads: ${newRows.length} new permit(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": "f6d162e9-4062-4882-9b44-b5f331e2702e",
      "name": "Keep only new matching leads"
    },
    {
      "parameters": {
        "fieldToSplitOut": "newRows",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1560,
        140
      ],
      "id": "b80c49e0-5fd2-4895-8b6f-df953611ec96",
      "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 }}",
            "taxpayerNumber": "={{ $json.taxpayerNumber }}",
            "outletNumber": "={{ $json.outletNumber }}",
            "businessName": "={{ $json.businessName }}",
            "locationName": "={{ $json.locationName }}",
            "permitIssueDate": "={{ $json.permitIssueDate }}",
            "naicsCode": "={{ $json.naicsCode }}",
            "organizationType": "={{ $json.organizationType }}",
            "city": "={{ $json.city }}",
            "zipCode": "={{ $json.zipCode }}",
            "locationAddress": "={{ $json.locationAddress }}",
            "retrievedAt": "={{ $json.retrievedAt }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "uniqueId",
              "displayName": "uniqueId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "taxpayerNumber",
              "displayName": "taxpayerNumber",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "outletNumber",
              "displayName": "outletNumber",
              "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": "locationName",
              "displayName": "locationName",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "permitIssueDate",
              "displayName": "permitIssueDate",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "naicsCode",
              "displayName": "naicsCode",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "organizationType",
              "displayName": "organizationType",
              "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": "zipCode",
              "displayName": "zipCode",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "locationAddress",
              "displayName": "locationAddress",
              "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": "f8ba0c69-6a4c-4360-971e-750b62d505a8",
      "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": "530b945c-3e1f-4126-8dad-2a189a8661fc",
              "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": "81ecf4de-22c1-44cc-ab63-09924f5909b5",
      "name": "Should send a summary?"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "44a34867-c002-421a-a580-aa3efad7a31e",
                    "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": "8413ec07-4042-42c6-baaf-2cf8fb6c27fc",
                    "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": "9b0e603c-a945-4682-9ea8-88aac3e79b49",
      "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": "164a2e36-e526-487e-b4e8-17f064708394",
      "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": "b911333d-c6e0-477d-88ad-e9812cad05d9",
      "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": "abc1c2e3-303b-46c4-aed9-ddcc2c1a2ca6",
      "name": "Format a run error"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "81e1591f-a9c0-437e-8167-8281e34b6834",
                    "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": "6f12a9cc-2eb1-4e5f-8da6-0e0eb9a58a0c",
                    "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": "81787131-2811-44a7-8303-053ecf831408",
      "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": "0620d907-e69f-4d10-96d7-db94bdc59fde",
      "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": "a2ecb51b-c687-47c5-9a08-6b30aa0e3eba",
      "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 Monday 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 TX new restaurant permits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch TX new restaurant permits": {
      "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": "bb37d423-c22f-4e56-889a-07b60858db4e",
  "meta": {
    "templateCredsSetupCompleted": false,
    "instanceId": "loomwise-template-export"
  },
  "tags": []
}
