{
  "name": "Tech-stack signal: domains to Google Sheets with email, reviews, chat and BNPL columns",
  "nodes": [
    {
      "parameters": {},
      "id": "6bfd2fb4-4a6e-51cb-b247-014d8f8ee84d",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        -100
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 10
            }
          ]
        }
      },
      "id": "9fd86030-36d7-5cd2-b911-565816653a0e",
      "name": "Every day at 10:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        100
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Domains",
          "mode": "name"
        },
        "options": {}
      },
      "id": "109a4ca7-6676-549f-bcee-081086d2d40f",
      "name": "Read the Domains tab",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        240,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_YOUR_GOOGLE_SHEETS_CREDENTIAL",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Send only rows that have not been looked up yet (empty checkedAt), so re-runs cost nothing.\nconst domains = [...new Set(\n  $input.all()\n    .map((i) => i.json)\n    .filter((r) => r.domain && !r.checkedAt)\n    .map((r) => String(r.domain).trim())\n    .filter(Boolean),\n)].slice(0, 5000);\nif (domains.length === 0) return [];\nreturn [{ json: { domains } }];"
      },
      "id": "7a538cfa-4596-5594-b52e-d961be48e749",
      "name": "Pick new domains",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        0
      ]
    },
    {
      "parameters": {
        "resource": "Actors",
        "operation": "Run actor and get dataset",
        "actorId": {
          "__rl": true,
          "value": "XRAfTUezTqloGBQwp",
          "mode": "id"
        },
        "customBody": "={{ JSON.stringify({ domains: $json.domains, checkWebsite: true, checkSsl: true, checkRegistration: true }) }}",
        "maxTotalChargeUsd": 2
      },
      "id": "1d5ba5d4-4107-52c1-930d-821d13fc476d",
      "name": "Look up the domains",
      "type": "@apify/n8n-nodes-apify.apify",
      "typeVersion": 2,
      "position": [
        720,
        0
      ],
      "credentials": {
        "apifyApi": {
          "id": "REPLACE_WITH_YOUR_APIFY_CREDENTIAL",
          "name": "Apify account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Flatten lists to text and add the columns a tech-stack segment needs.\n// `domain` is the exact value from your sheet, so the update hits the right row.\nconst list = (a) => (Array.isArray(a) ? a.join(', ') : a ?? '');\nconst cat = (d, c) => (d.techCategories ?? {})[c] ?? [];\nconst asked = $('Pick new domains').first().json.domains;\nconst byInput = new Map($input.all().map((i) => i.json).filter((d) => d.input).map((d) => [d.input, d]));\nconst today = new Date().toISOString().slice(0, 10);\nreturn asked.map((domain) => {\n  const d = byInput.get(domain);\n  // No row back (the site timed out, or the run hit its cost cap): leave checkedAt empty so tomorrow's run retries it.\n  // Domains without a row are not charged.\n  if (!d) return { json: { domain, resolves: 'not checked yet (see the Apify run log)', checkedAt: '' } };\n  const homepageRead = !(d.errors ?? {}).tech;\n  const email = cat(d, 'Marketing automation');\n  const reviews = cat(d, 'Reviews');\n  return { json: {\n    domain,\n    resolves: d.resolves,\n    homepageRead,\n    shopPlatform: list(d.ecommerce),\n    emailMarketing: list(email),\n    reviews: list(reviews),\n    liveChat: list(cat(d, 'Live chat')),\n    bnpl: list(cat(d, 'Buy now pay later')),\n    cookieConsent: list(cat(d, 'Cookie compliance')),\n    analytics: list(d.analytics),\n    // The segment from the article. Homepage only: check the product page before you pitch a review app.\n    klaviyoNoReviewApp: homepageRead && email.includes('Klaviyo') && reviews.length === 0,\n    emailProvider: d.emailProvider ?? '',\n    dmarcPolicy: d.dmarcPolicy ?? (d.resolves ? 'missing' : ''),\n    cdn: d.cdn ?? '',\n    siteTitle: d.title ?? '',\n    checkedAt: today,\n  } };\n});"
      },
      "id": "200d9aa5-1eb7-5f33-a10a-03385f57fa66",
      "name": "Flatten for the sheet",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        0
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_YOUR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Domains",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "domain"
          ],
          "schema": []
        },
        "options": {}
      },
      "id": "f415d5a3-99d2-508c-af12-93326f3c9ba0",
      "name": "Write results to the sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1200,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "REPLACE_WITH_YOUR_GOOGLE_SHEETS_CREDENTIAL",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "content": "## Read your list\nOnly rows with an empty `checkedAt` are sent.",
        "height": 460,
        "width": 660,
        "color": 7
      },
      "id": "be0e0047-a2d5-5d09-962d-1a951f322ed8",
      "name": "Read",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -230
      ]
    },
    {
      "parameters": {
        "content": "## Look up and write back\nOne row per domain, matched on `domain`.",
        "height": 290,
        "width": 720,
        "color": 7
      },
      "id": "a94e6b28-49f0-5345-aa7c-aa012604c4cd",
      "name": "Look up",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        670,
        -130
      ]
    },
    {
      "parameters": {
        "content": "## Tech-stack signal: segment a list of shops by the tools on their homepage\n\nThe workflow from locaihost.org/blog/tech-stack-signal-shopify-klaviyo/ . Adds `emailMarketing`, `reviews`, `liveChat`, `bnpl`, `cookieConsent` and a `klaviyoNoReviewApp` flag to each row, so a sheet filter gives you the segment.\n\nFor sales and marketing teams qualifying leads, agencies scoping new clients, and anyone with a column of company websites to enrich.\n\n### How it works\n1. Start it by hand or let the daily schedule run. It reads the **Domains** tab and keeps rows whose `checkedAt` is empty.\n2. The **domain-intel** Actor checks each domain's public DNS records and homepage: email provider, SPF and DMARC, DNS host, hosting, CDN, CMS, shop platform, analytics, and the rest of the 3,111 technologies it recognises, and SSL expiry.\n3. A Code node turns the lists into text and writes one row back per domain, matched on the `domain` column.\n\n### Setup\n1. Add your Apify API token as an *Apify API* credential.\n2. Create a Google Sheet with a tab named **Domains** and the header row `domain`, `checkedAt`. Domains, URLs and email addresses all work.\n3. Paste the sheet ID into both Sheets nodes and pick your Google credential.\n\n### Cost per run\n$3 per 1,000 domains, so 100 new domains cost $0.30. Already-checked rows cost nothing. The node caps each run at $2. The Apify free plan checks up to 100 domains per run. Only public records are read.\n\n**Apify node:** this uses the verified Apify node (`@apify/n8n-nodes-apify`). n8n Cloud installs it from the canvas; on self-hosted n8n add it under Settings > Community nodes.\n\n### Customization tips\nSet `checkWebsite` to false for a faster DNS-only run, or remove columns you don't need in **Flatten for the sheet**. For Airtable, swap the last node for an Airtable *Create or update* node matched on `domain`.\n\nMade by locaihost data (locaihost.org).",
        "height": 800,
        "width": 580
      },
      "id": "20f84cfb-b49b-5ed5-b152-c1e86c434f90",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -680,
        -240
      ]
    }
  ],
  "connections": {
    "Run manually": {
      "main": [
        [
          {
            "node": "Read the Domains tab",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every day at 10:00": {
      "main": [
        [
          {
            "node": "Read the Domains tab",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the Domains tab": {
      "main": [
        [
          {
            "node": "Pick new domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick new domains": {
      "main": [
        [
          {
            "node": "Look up the domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look up the domains": {
      "main": [
        [
          {
            "node": "Flatten for the sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten for the sheet": {
      "main": [
        [
          {
            "node": "Write results to the sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
