{
  "name": "Deduplicate weekly federal award winners into Google Sheets with a digest",
  "nodes": [
    {
      "parameters": {
        "content": "### How it works\nThis workflow helps federal business-development, competitive-intelligence, and partner-recruiting teams track who just won work. Every Monday it runs the Loomwise Federal Contract Awards Actor for your winner states, NAICS prefixes, agencies, keywords, and minimum amount. It matches each award on the USAspending generated award ID against your Google Sheet and appends only new winners. The digest flags awards at or above your large-award threshold (default $1,000,000) and can email or post the count plus top recipients. Failures after retries send an alert instead of duplicating awards.\n\n### Setup\n1. Install `@apify/n8n-nodes-apify` (v0.6+; https://docs.apify.com/integrations/n8n) and connect your Apify API credential. Do not paste an API token into a node.\n2. Open https://apify.com/loomwise/federal-contract-awards and try a small run.\n3. Create a Google Sheet with header row: uniqueId, generatedAwardId, awardId, recipientName, awardAmount, awardDate, awardingAgency, naicsCode, naicsDescription, setAside, recipientCity, recipientState, awardUrl, isLargeAward, retrievedAt.\n4. Connect Google Sheets OAuth. In Configure filters, paste the spreadsheet ID and set recipient states, NAICS, optional agencies and keywords, minAmount, largeAwardThreshold, and maxResults (100 for a first run).\n5. Connect Gmail or Slack and replace the placeholders. Set sendSummaryNotification to false to skip the digest.\n6. Run once manually, then activate. The Actor is pay-per-result. Awards can appear a few days after the official date because agencies report on a lag. More data tools: https://loomwise.app/data/\n\n### Customization\nChange the winner state, raise minAmount, or add keywords such as roofing or cybersecurity. Turn includeIdvs on in the Apify input if you also want new IDIQ or BPA vehicles (off by default).\n",
        "height": 740,
        "width": 520,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        -100
      ],
      "id": "bc766b7d-e5fe-41b2-8373-83347a339da6",
      "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": "e8527464-ba01-4e52-b861-a1a026d57587",
      "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": "e52f22a2-3d88-4ef9-8ff7-dbc0b2648588",
      "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": "e4a4ebf1-d769-4045-95ee-26a4980ba531",
      "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": "630228f4-ea1a-401b-a4fb-1ffe0de23af8",
      "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": "b53a7ce5-8e95-4f54-b694-e77ae1d0116c",
      "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": "f9476564-ca91-4e78-9802-bac36e90ae40",
      "name": "\u26a0 Replace placeholders before you run"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        40,
        220
      ],
      "id": "0df03dd9-4ff6-4d6b-a6cd-eb333c90d02d",
      "name": "Every Monday 8am"
    },
    {
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "c80937a0-a5dd-4e3a-8c5b-e83b68fc578d",
              "name": "workflowTitle",
              "value": "Weekly federal contract awards",
              "type": "string"
            },
            {
              "id": "9f9f15ef-41c9-4c41-84a4-c6a56dcd7541",
              "name": "spreadsheetId",
              "value": "REPLACE_WITH_YOUR_GOOGLE_SHEET_ID",
              "type": "string"
            },
            {
              "id": "b1d7d216-5e7d-4e1f-b5bd-6dabf1308729",
              "name": "sheetName",
              "value": "Sheet1",
              "type": "string"
            },
            {
              "id": "b7b5962d-8cd2-445e-9959-a0a265f06798",
              "name": "uniqueIdField",
              "value": "generatedAwardId",
              "type": "string"
            },
            {
              "id": "b469f43c-7081-4325-ae00-b73c9bb49d28",
              "name": "withinDays",
              "value": 7,
              "type": "number"
            },
            {
              "id": "ccb91346-6d6e-443e-9fff-fc8a45125098",
              "name": "recipientStatesCsv",
              "value": "TX",
              "type": "string"
            },
            {
              "id": "b20122a6-0860-4642-a8fe-89bf0b46da96",
              "name": "naicsCodesCsv",
              "value": "23",
              "type": "string"
            },
            {
              "id": "7376fe1d-8e45-467d-8a59-83663978fefc",
              "name": "agenciesCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "860fcf83-bd53-4849-8660-7ab435479fd8",
              "name": "keywordsCsv",
              "value": "",
              "type": "string"
            },
            {
              "id": "eb47d0f4-834c-4cc8-8c6a-987324bd54b3",
              "name": "minAmount",
              "value": 100000,
              "type": "number"
            },
            {
              "id": "c258399f-1d68-437f-9b13-88cd39651808",
              "name": "largeAwardThreshold",
              "value": 1000000,
              "type": "number"
            },
            {
              "id": "7bbaac3b-94b6-41e6-a331-305795c797d6",
              "name": "maxResults",
              "value": 100,
              "type": "number"
            },
            {
              "id": "039eefdb-fdb2-4b57-ba75-b0d9d5849a27",
              "name": "sendSummaryNotification",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "126cfdaa-c11b-4d18-a392-75b9dba3ccdd",
              "name": "notificationChannel",
              "value": "gmail",
              "type": "string"
            },
            {
              "id": "d9117f93-1d42-499e-b718-be1d77d0f105",
              "name": "notifyEmail",
              "value": "REPLACE_WITH_YOUR_EMAIL@example.com",
              "type": "string"
            },
            {
              "id": "94cefb91-21fd-4169-8f79-0764b75d6dd7",
              "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": "1894ab2a-8e03-4aa6-8fc0-f355a37f1246",
      "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": "bc5c0a60-0e3d-41fa-99d7-2807146fe643",
      "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": "2c9d214d-49a4-4c16-adc6-7b4ed71cde97",
      "name": "Collect existing IDs"
    },
    {
      "parameters": {
        "operation": "Run actor and get dataset",
        "actorId": "loomwise~federal-contract-awards",
        "customBody": "={{ JSON.stringify({ withinDays: $json.withinDays, recipientStates: String($json.recipientStatesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), naicsCodes: String($json.naicsCodesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), agencies: String($json.agenciesCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), keywords: String($json.keywordsCsv||\"\").split(\",\").map(s=>s.trim()).filter(Boolean), minAmount: $json.minAmount, maxResults: $json.maxResults, excludeIndividuals: true, includeDetails: true, includeIdvs: false }) }}",
        "timeout": null,
        "memory": 1024,
        "resource": "Actors",
        "actorInputMode": "json",
        "authentication": "apifyApi"
      },
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 1,
      "position": [
        1040,
        220
      ],
      "id": "f0ffefb6-09e8-4a7e-81da-efd8d4500c43",
      "name": "Fetch federal contract awards",
      "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 passesExtraFilters(row, cfg) {\n  const minAmount = Number(cfg.minAmount);\n  if (Number.isFinite(minAmount) && minAmount > 0) {\n    if (!(Number(row.awardAmount) >= minAmount)) return false;\n  }\n  const keywords = String(cfg.keywordsCsv || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);\n  if (keywords.length) {\n    const hay = `${row.recipientName || ''} ${row.description || ''} ${row.naicsDescription || ''}`.toLowerCase();\n    if (!keywords.some((k) => hay.includes(k))) return false;\n  }\n  return true;\n}\nfunction mapRow(row, uniqueId) {\n  const largeAt = Number(cfg.largeAwardThreshold) || 1000000;\n  const amount = Number(row.awardAmount);\n  return {\n    uniqueId,\n    generatedAwardId: row.generatedAwardId || '',\n    awardId: row.awardId || '',\n    recipientName: row.recipientName || '',\n    awardAmount: Number.isFinite(amount) ? amount : '',\n    awardDate: row.awardDate || '',\n    awardingAgency: row.awardingAgency || '',\n    naicsCode: row.naicsCode || '',\n    naicsDescription: row.naicsDescription || '',\n    setAside: row.setAside || '',\n    recipientCity: row.recipientCity || '',\n    recipientState: row.recipientState || '',\n    awardUrl: row.awardUrl || '',\n    isLargeAward: Number.isFinite(amount) && amount >= largeAt,\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.isLargeAward ? '[LARGE] ' : ''}${row.recipientName || 'n/a'} \u2014 $${row.awardAmount || 'n/a'} from ${row.awardingAgency || 'n/a'} (${row.recipientCity || ''} ${row.recipientState || ''}) ${row.awardUrl || ''}`);\nconst intro = `New federal contract awards appended this week: ${newRows.length}. At or above your large-award threshold: ${newRows.filter(r => r.isLargeAward).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 federal contract awards matched your filters (or they were already in the sheet).\";\nconst subject = `Federal awards: ${newRows.length} new, ${newRows.filter(r => r.isLargeAward).length} large`;\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": "0d48d7c9-294b-493b-81f0-7789a28647ec",
      "name": "Keep only new matching leads"
    },
    {
      "parameters": {
        "fieldToSplitOut": "newRows",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1560,
        140
      ],
      "id": "0c5cc2d4-6a56-48dc-ac4a-0fbda57050c9",
      "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 }}",
            "generatedAwardId": "={{ $json.generatedAwardId }}",
            "awardId": "={{ $json.awardId }}",
            "recipientName": "={{ $json.recipientName }}",
            "awardAmount": "={{ $json.awardAmount }}",
            "awardDate": "={{ $json.awardDate }}",
            "awardingAgency": "={{ $json.awardingAgency }}",
            "naicsCode": "={{ $json.naicsCode }}",
            "naicsDescription": "={{ $json.naicsDescription }}",
            "setAside": "={{ $json.setAside }}",
            "recipientCity": "={{ $json.recipientCity }}",
            "recipientState": "={{ $json.recipientState }}",
            "awardUrl": "={{ $json.awardUrl }}",
            "isLargeAward": "={{ $json.isLargeAward }}",
            "retrievedAt": "={{ $json.retrievedAt }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "uniqueId",
              "displayName": "uniqueId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "generatedAwardId",
              "displayName": "generatedAwardId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "awardId",
              "displayName": "awardId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "recipientName",
              "displayName": "recipientName",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "awardAmount",
              "displayName": "awardAmount",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "awardDate",
              "displayName": "awardDate",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "awardingAgency",
              "displayName": "awardingAgency",
              "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": "naicsDescription",
              "displayName": "naicsDescription",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "setAside",
              "displayName": "setAside",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "recipientCity",
              "displayName": "recipientCity",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "recipientState",
              "displayName": "recipientState",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "awardUrl",
              "displayName": "awardUrl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "isLargeAward",
              "displayName": "isLargeAward",
              "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": "a7c1c539-f6e7-4900-bc60-d58cd08ab931",
      "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": "758d7e6f-75b7-4227-8e16-29f452373e48",
              "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": "13a061a0-41fa-426a-882e-8d5bbaeafc93",
      "name": "Should send a summary?"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "16d1c5c3-745a-4bdd-8f18-530242054c57",
                    "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": "b5c45c94-f522-44b5-985c-f96062f8413c",
                    "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": "c1c4c146-241c-473e-b895-14404e68c059",
      "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": "a1b61659-cbdd-4bea-8ba6-b335349159fb",
      "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": "80dd0d3e-dfaf-4897-bd07-71ac44cd13ef",
      "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": "4eb1be6f-95b3-44bb-ba88-8f58d938652d",
      "name": "Format a run error"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "d0f0602b-7643-44e9-aead-cec464c482f9",
                    "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": "82ff2520-aba0-4a9d-97c2-5a9176e2f428",
                    "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": "07c120b4-03e9-4b97-bdd1-f74cf3ddf548",
      "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": "f6de6aaa-7e0d-4289-b042-944de55d1fd2",
      "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": "317e8852-b85b-4cef-8e04-70c3bf748691",
      "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 8am": {
      "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 federal contract awards",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch federal contract awards": {
      "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": "a490c2f9-f3f3-49dd-a95a-e02b47493831",
  "meta": {
    "templateCredsSetupCompleted": false,
    "instanceId": "loomwise-template-export"
  },
  "tags": []
}
