{
  "name": "Resolve company domains to LinkedIn company records with Apify",
  "active": false,
  "settings": { "executionOrder": "v1", "executionTimeout": 1200 },
  "pinData": {},
  "tags": [],
  "nodes": [
    { "id": "start", "name": "Start manually", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [0, 0], "parameters": {} },
    {
      "id": "prepare", "name": "Prepare domains", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [220, 0],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const supplied = $input.first().json.domains;\nconst domains = supplied === undefined ? ['stripe.com', 'vercel.com', 'gitlab.com'] : supplied;\nif (!Array.isArray(domains) || domains.length === 0 || domains.length > 10) throw new Error('Supply between 1 and 10 domains for this starter workflow.');\nfor (const domain of domains) {\n  if (typeof domain !== 'string' || !domain.trim()) throw new Error('Each domain must be a nonempty string.');\n  if (!/^(?:https?:\\/\\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z]{2,}(?::[0-9]{1,5})?(?:[/?#][^\\s]*)?$/i.test(domain.trim())) throw new Error('Invalid website domain.');\n}\nreturn [{ json: { domains: domains.map(domain => domain.trim()), maxDomains: domains.length, mode: 'resolve', includeUnresolved: true, concurrency: 3 } }];"
      }
    },
    {
      "id": "start-actor", "name": "Start actor", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [440, 0],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/george.the.developer~linkedin-company-by-domain/runs",
        "method": "POST", "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": { "parameters": [
          { "name": "maxTotalChargeUsd", "value": "0.10" },
          { "name": "timeout", "value": "900" },
          { "name": "memory", "value": "512" },
          { "name": "waitForFinish", "value": "0" }
        ] },
        "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json) }}",
        "options": { "timeout": 60000, "response": { "response": { "responseFormat": "json" } } }
      }
    },
    {
      "id": "route", "name": "Route run", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [660, 0],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const data = $input.first().json.data;\nif (!data || typeof data.id !== 'string' || !data.id) throw new Error('Malformed actor run response.');\nif (data.status === 'SUCCEEDED') {\n  if (typeof data.defaultDatasetId !== 'string' || !data.defaultDatasetId) throw new Error('Successful run has no dataset ID.');\n  return [{ json: { ...data, routeIndex: 0 } }];\n}\nif (!['READY', 'RUNNING', 'ABORTING', 'TIMING-OUT'].includes(data.status)) throw new Error('Actor failed or returned an unsupported status: ' + data.status);\nconst started = Date.parse(data.startedAt || data.createdAt);\nif (!Number.isFinite(started) || started > Date.now() + 60000) throw new Error('Run response has an invalid timestamp.');\nif (Date.now() - started > 1020000) throw new Error('Polling exceeded seventeen minutes; inspect the run in Apify Console.');\nreturn [{ json: { ...data, routeIndex: 1 } }];"
      }
    },
    {
      "id": "branch", "name": "Branch by run status", "type": "n8n-nodes-base.switch", "typeVersion": 3.2, "position": [880, 0],
      "parameters": { "mode": "expression", "numberOutputs": 2, "output": "={{ $json.routeIndex }}" }
    },
    {
      "id": "wait", "name": "Wait before polling", "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [1100, 180],
      "parameters": { "resume": "timeInterval", "amount": 15, "unit": "seconds" }
    },
    {
      "id": "status", "name": "Get run status", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1320, 180],
      "parameters": {
        "url": "={{ 'https://api.apify.com/v2/actor-runs/' + $json.id }}",
        "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth",
        "options": { "timeout": 60000, "response": { "response": { "responseFormat": "json" } } }
      }
    },
    {
      "id": "dataset", "name": "Get company rows", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1100, -120], "alwaysOutputData": true,
      "parameters": {
        "url": "={{ 'https://api.apify.com/v2/datasets/' + $json.defaultDatasetId + '/items?clean=true&format=json&limit=1000' }}",
        "authentication": "genericCredentialType", "genericAuthType": "httpHeaderAuth",
        "options": { "timeout": 60000, "response": { "response": { "responseFormat": "json" } } }
      }
    },
    {
      "id": "review", "name": "Review company rows", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1320, -120],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nif (items.length === 0) throw new Error('The actor returned no company rows.');\nreturn items.map((item, index) => {\n  const row = item.json;\n  if (!row || typeof row.domain !== 'string' || !row.domain) throw new Error('Empty or malformed company dataset.');\n  const validCompanyUrl = typeof row.linkedinUrl === 'string' && /^https:\\/\\/(?:www\\.)?linkedin\\.com\\/company\\/[^/?#\\s]+(?:[/?#]|$)/i.test(row.linkedinUrl);\n  return { json: { ...row, needsReview: !(['high', 'medium'].includes(row.confidence) && validCompanyUrl) }, pairedItem: { item: index } };\n});"
      }
    },
    {
      "id": "setup", "name": "Setup and pricing", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [-950, -390],
      "parameters": {
        "content": "## Quick overview\nThis workflow takes a list of company website domains, runs the Apify “LinkedIn Company by Domain” actor to resolve each domain to a LinkedIn company page, and returns the resulting dataset with a needsReview flag for low-confidence or invalid matches.\n\n## How it works\n1. Starts when you run the workflow manually.\n2. Validates and normalizes 1–10 input domains (or uses sample domains) and builds the Apify actor input, including concurrency and whether to keep unresolved results.\n3. Starts the Apify actor run via the Apify API with a charge cap and run timeout.\n4. Polls the existing Apify run every 15 seconds, stopping on a failed terminal status or after a 17-minute polling age limit. The overall workflow execution limit is 20 minutes.\n5. Fetches the completed results from the Apify dataset associated with the successful run.\n6. Validates each returned row and adds a needsReview flag when confidence is not high/medium or the LinkedIn company URL is missing/invalid.\n\n## Setup\n1. Create an Apify account and generate an Apify API token.\n2. In n8n, add an HTTP Header Auth credential with header name Authorization and value Bearer YOUR_APIFY_TOKEN, and select it on the Apify HTTP requests.\n3. Provide 1–10 domains as input to the workflow or edit the sample list in the domain preparation step before executing manually.\n4. Start with the three-domain sample and inspect the results before scaling. The starter uses a $0.10 maximum actor charge and a 900-second actor timeout. Review current actor pricing and the linked setup guide before changing limits; preserve working time beyond the actor shutdown reserve.\n\n## Requirements\nAn n8n instance, your own Apify account and private Header Auth credential. Actor execution can incur charges on your Apify account; spreadsheet destinations require their own credentials.\n\n## Customization\nExport final records as CSV or connect a Google Sheets node using your own spreadsheet credentials. Review domain coverage and needsReview flags before using the results.\n\n## Additional information\nThe charge cap can produce partial results. This workflow returns company research, not individual employee profiles or emails. The review flag checks returned fields and does not independently verify identity. Setup and verification scope: https://the-ai-entrepreneur-ai-hub.github.io/apify-company-research-workflows/ . Maintained by the developer of the linked actor. Contract tests and three n8n 2.42.4 CLI scenarios passed with a local mock API; live actor resolution was not tested in those checks.",
        "height": 1550, "width": 750
      }
    }
  ],
  "connections": {
    "Start manually": { "main": [[{ "node": "Prepare domains", "type": "main", "index": 0 }]] },
    "Prepare domains": { "main": [[{ "node": "Start actor", "type": "main", "index": 0 }]] },
    "Start actor": { "main": [[{ "node": "Route run", "type": "main", "index": 0 }]] },
    "Route run": { "main": [[{ "node": "Branch by run status", "type": "main", "index": 0 }]] },
    "Branch by run status": { "main": [[{ "node": "Get company rows", "type": "main", "index": 0 }], [{ "node": "Wait before polling", "type": "main", "index": 0 }]] },
    "Wait before polling": { "main": [[{ "node": "Get run status", "type": "main", "index": 0 }]] },
    "Get run status": { "main": [[{ "node": "Route run", "type": "main", "index": 0 }]] },
    "Get company rows": { "main": [[{ "node": "Review company rows", "type": "main", "index": 0 }]] }
  }
}
