Turn Signals Into Personalized LinkedIn Outreach — Without the Manual Research

Mohamed ChahinJuly 3, 20267 min read

Good LinkedIn outreach takes ~15–20 minutes of research per prospect, so reps send generic notes or never get to it. This Twain + n8n workflow turns a signal into a researched, personalized LinkedIn intro and follow-ups in ~20 seconds — drafted and queued, with you still the sender.

Everyone knows what good LinkedIn outreach looks like. You read the person first — their role, what their company is doing, why now — and you write a note that could only have been sent to them. It lands because it's obviously not a template.

The problem is the math. Doing that research by hand is 15 to 20 minutes per prospect. So one of two things happens: reps fire off a generic connection note that gets ignored or flagged, or they never get to it at all and the signal goes cold.

This Twain + n8n workflow removes the tradeoff. A signal comes in, and in ~20 seconds Twain researches the prospect and writes a personalized LinkedIn intro plus follow-ups — then n8n drops it in front of you as a draft. Personalized outreach at the speed of generic, with the quality of hand-written. You stay the sender.

The research tax on LinkedIn

The research is the part that decides whether outreach works — and the part that doesn't scale. A rep with a list of 40 signals this week is looking at 10+ hours of profile-reading before a single message goes out. Nobody has that, so the research gets skipped and the messages get worse.

That's the trap. The fix isn't "send more, faster" — that's how inboxes got numb to outreach in the first place. The fix is to make the researched version the easy one, so the personal message is the one that actually goes out.

The outcome: a signal in, a draft out

You point a trigger at Twain — a HubSpot segment, a Clay list, a webhook, anything that means "reach out now." For each prospect, Twain reads the person and their company and writes a LinkedIn intro built on a real angle, plus the follow-ups behind it.

What used to be 15–20 minutes of manual digging becomes a draft waiting in Slack about 20 seconds later. Same research, same personalization — none of the grind. Run it across a list and a week of outreach prep collapses into the time it takes to read and approve.

What you actually get

The point isn't automation for its own sake. It's a specific set of outcomes that are hard to get any other way:

  • Personalized LinkedIn at scale — every prospect gets copy built around a real reason to reach out, not a mail-merge first name. The thing that doesn't scale by hand now does.
  • Research without the manual work — Twain reads the profile and the company so you don't have to. The 15–20 minutes per prospect goes to zero. Twain works from the LinkedIn profile it finds during research, and an optional lookup can resolve one from just an email when the signal didn't include it — so more of your leads are reachable.
  • Signal-based, not spray-and-pray — outreach fires off the trigger that says now matters, so you're reaching the right people at the right moment.
  • Fits your stack — driven by the tools you already run: HubSpot, Clay, a webhook, Slack for review.
  • Optional push to a sending tool — when you want it, the same copy can flow straight into a LinkedIn sender like HeyReach. Off by default; you turn it on.

You stay the sender

This is the part that matters for anyone who's been burned by LinkedIn automation. The workflow stops at a draft. Nothing auto-sends, nothing goes out under your name without you seeing it first.

Twain does the research and writes the copy; the draft lands in Slack with the person, their company, the signal that surfaced them, and the full intro plus follow-ups. You read it and send it yourself on LinkedIn. No spray-and-pray, no off-brand AI message you didn't approve, no surprise activity on your account.

When you'd rather route the approved copy into a dedicated sender (HeyReach, La Growth Machine, Dripify) for volume, there's a branch for that — but it ships disabled, because those tools run through your own account and carry real ToS and ban risk. The default keeps you in control of every message that goes out.

Where it fits

This earns its place anywhere personalized LinkedIn outreach is worth getting right but the research won't scale:

  • Founder-led and SDR motions — researched, personal copy in seconds, sent from your own profile so it reads as you.
  • Signal-based outbound — a HubSpot segment, a Clay list, or any trigger that means "this account is in-market now."
  • Teams wary of LinkedIn automation — you get the speed of automation and stay the human who hits send. Best of both.

What goes into the intro and follow-ups, and how they read, is decided entirely in Twain. You set up a LinkedIn campaign once and describe the job in plain language — who you're reaching, what to research, and what the messages should do. Every future signal gets copy built to that brief, no rewiring. See Campaign Brief and Deep Research to go deeper.

Import the template

Want to run it yourself? Copy the sanitized JSON below — secrets removed, live IDs replaced with placeholders — and import it into n8n. You attach your own connections (Twain, Slack, an optional source and sender) and paste your campaign ID after import.

{
  "name": "Twain — LinkedIn Signal Outreach",
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": true,
    "binaryMode": "separate"
  },
  "nodes": [
    {
      "id": "signalWebhook",
      "name": "Signal Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [240, 520],
      "parameters": {
        "httpMethod": "POST",
        "path": "linkedin-signal",
        "options": {}
      }
    },
    {
      "id": "linkedinSettings",
      "name": "LinkedIn Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1000, 520],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "campaign_id",
              "name": "campaign_id",
              "value": "REPLACE_WITH_LINKEDIN_CAMPAIGN_ID",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      }
    },
    {
      "id": "normalizeLinkedinLead",
      "name": "Normalize LinkedIn Lead",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1380, 520],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const src = $json.body ?? $json ?? {};\nconst PERSONAL = [\"gmail.com\",\"googlemail.com\",\"outlook.com\",\"hotmail.com\",\"live.com\",\"msn.com\",\"yahoo.com\",\"ymail.com\",\"icloud.com\",\"me.com\",\"mac.com\",\"proton.me\",\"protonmail.com\",\"aol.com\",\"gmx.com\",\"gmx.net\",\"zoho.com\",\"pm.me\",\"mail.com\",\"yandex.com\"];\nconst str = (v) => (v === undefined || v === null) ? \"\" : String(v);\nconst firstNonEmpty = (...vals) => { for (const v of vals) { const s = str(v).trim(); if (s) return s; } return \"\"; };\nconst linkedin_url = firstNonEmpty(src.linkedin_url, src.linkedin_profile_url, src.linkedin);\nconst name = firstNonEmpty(src.name, src.full_name, src.displayName);\nconst email = firstNonEmpty(src.email, src.work_email).toLowerCase();\nconst company = firstNonEmpty(src.company, src.company_name);\nconst company_domain = firstNonEmpty(src.company_domain, src.companyDomain);\nconst signal = firstNonEmpty(src.signal, src.signal_context, src.reason, src.context);\nconst email_domain = (email.split(\"@\")[1] || \"\").toLowerCase();\nconst is_personal_email = PERSONAL.includes(email_domain);\nconst has_work_email = Boolean(email) && !is_personal_email;\nconst has_linkedin_url = Boolean(linkedin_url);\nconst uid = firstNonEmpty(src.uid, linkedin_url, email, name);\nconst campaign_id = ($('LinkedIn Settings').first().json.campaign_id || \"REPLACE_WITH_LINKEDIN_CAMPAIGN_ID\");\nreturn {\n  uid: uid,\n  linkedin_url: linkedin_url,\n  name: name,\n  email: email,\n  company: company,\n  company_domain: company_domain,\n  signal: signal,\n  email_domain: email_domain,\n  is_personal_email: is_personal_email,\n  has_work_email: has_work_email,\n  has_linkedin_url: has_linkedin_url,\n  campaign_id: campaign_id\n};"
      }
    },
    {
      "id": "dedupeLeads",
      "name": "Dedupe Leads",
      "type": "n8n-nodes-base.removeDuplicates",
      "typeVersion": 2,
      "position": [1760, 520],
      "parameters": {
        "operation": "removeItemsSeenInPreviousExecutions",
        "dedupeValue": "={{ $json.uid }}",
        "options": {
          "scope": "node",
          "historySize": 100000
        }
      }
    },
    {
      "id": "needsLinkedinLookup",
      "name": "Needs LinkedIn Lookup?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [2140, 520],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "needs-li-1",
              "leftValue": "={{ !$('Normalize LinkedIn Lead').item.json.has_linkedin_url && Boolean($('Normalize LinkedIn Lead').item.json.email) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "id": "linkedinLookupProxycurl",
      "name": "LinkedIn Lookup — Proxycurl",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [2520, -360],
      "disabled": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "url": "=https://nubela.co/proxycurl/api/linkedin/profile/resolve/email?email={{ encodeURIComponent($('Normalize LinkedIn Lead').item.json.email) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "linkedinLookupPeopleDataLabs",
      "name": "LinkedIn Lookup — People Data Labs",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [2520, -140],
      "disabled": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "url": "=https://api.peopledatalabs.com/v5/person/enrich?email={{ encodeURIComponent($('Normalize LinkedIn Lead').item.json.email) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "linkedinLookupApollo",
      "name": "LinkedIn Lookup — Apollo",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [2520, 80],
      "disabled": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "https://api.apollo.io/api/v1/people/match",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { email: $('Normalize LinkedIn Lead').item.json.email } }}",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "linkedinLookupFindymail",
      "name": "LinkedIn Lookup — Findymail",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [2520, 300],
      "disabled": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "https://app.findymail.com/api/search/reverse-email",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { email: $('Normalize LinkedIn Lead').item.json.email, with_profile: true } }}",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "applyResolvedLinkedin",
      "name": "Apply Resolved LinkedIn",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [2900, 520],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Resolve LinkedIn URL from the provider response.\n// Adjust the field below to match YOUR provider's response shape.\nconst resp = $json || {};\nconst linkedin_url = resp.linkedin_url ?? resp.linkedin_profile_url ?? resp.url ?? resp.contact?.linkedin_url ?? resp.profile?.linkedin_url ?? resp.person?.linkedin_url ?? resp.data?.linkedin_url ?? resp.response?.linkedin_url ?? \"\";\n\n// Carry forward all normalized fields, overriding only the LinkedIn ones.\nconst n = $('Normalize LinkedIn Lead').item.json;\nreturn {\n  ...n,\n  linkedin_url: linkedin_url,\n  has_linkedin_url: Boolean(linkedin_url)\n};\n"
      }
    },
    {
      "id": "resolvedContact",
      "name": "Resolved Contact",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [3280, 520],
      "parameters": {}
    },
    {
      "id": "hasLinkedinUrl",
      "name": "Has LinkedIn URL?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [3660, 520],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 3
          },
          "conditions": [
            {
              "leftValue": "={{ $json.has_linkedin_url }}",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "rightValue": "",
              "id": "has-li-1"
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "id": "twainGenerateSequence",
      "name": "Twain Generate Sequence",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [4040, 520],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput",
      "parameters": {
        "method": "POST",
        "url": "https://public.api.twain.ai/v2/Generate/Sequence",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  campaign_id: $('Normalize LinkedIn Lead').item.json.campaign_id,\n  contact: {\n    linkedin_profile_url: $json.linkedin_url,\n    ...($('Normalize LinkedIn Lead').item.json.has_work_email ? { work_email: $('Normalize LinkedIn Lead').item.json.email } : {}),\n  },\n  add_contact_to_campaign: true,\n} }}",
        "options": {
          "timeout": 180000
        }
      }
    },
    {
      "id": "buildLinkedinOutreach",
      "name": "Build LinkedIn Outreach",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [4420, 520],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const api = $json;\nconst norm = $(\"Normalize LinkedIn Lead\").item.json;\nconst messages = Array.isArray(api.messages) ? api.messages : [];\nconst first = messages[0] || {};\nconst research = api.research || {};\nconst person = research.person || {};\nconst company = research.company || {};\nconst warnings = api.warnings || {};\nconst categories = Array.isArray(warnings.categories) ? warnings.categories : [];\nconst str = (v) => (v === undefined || v === null) ? \"\" : String(v);\nconst linkedin_url = str((person && person.linkedin_profile_url) || norm.linkedin_url || \"\");\nconst linkedin_line = linkedin_url ? (\":link:  <\" + linkedin_url + \"|LinkedIn profile>\") : \"\";\nconst handleName = () => {\n  const m = linkedin_url.match(/linkedin\\.com\\/in\\/([^/?#]+)/i);\n  if (!m) return \"\";\n  let h = decodeURIComponent(m[1]).replace(/-[0-9a-f]{6,}$/i, \"\").replace(/[-_]+/g, \" \").trim();\n  if (!h) return \"\";\n  return h.split(\" \").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(\" \");\n};\nconst person_name = str(person.name) || str(norm.name) || handleName() || \"there\";\nconst intro_text = str(first.message);\nconst sequence_preview = messages.map((m) => str(m.message)).join(\"\\n\\n--------\\n\\n\");\nconst followups = messages.slice(1);\nconst followups_preview = followups.map((m) => str(m.message)).join(\"\\n\\n--------\\n\\n\");\nconst warning_line =\n  warnings && warnings.has_warnings\n    ? \":warning:  *Warnings:* \" +\n      ((categories || []).join(\", \") || \"flagged\") +\n      (warnings.summary ? \"  —  \" + warnings.summary : \"\")\n    : \"\";\nreturn {\n  intro_text: intro_text,\n  sequence_preview: sequence_preview || \"No messages returned.\",\n  followups_preview: followups_preview,\n  followups_count: followups.length,\n  step_count: messages.length,\n  person_name: person_name,\n  person_title: str(person.title),\n  company_name: str(company.name) || str(norm.company),\n  linkedin_url: linkedin_url,\n  linkedin_line: linkedin_line,\n  signal: str(norm.signal),\n  link: str(api.link),\n  warning_line: warning_line,\n  warnings_summary: str(warnings.summary)\n};"
      }
    },
    {
      "id": "slackLinkedinOutreachReady",
      "name": "Slack: LinkedIn outreach ready",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [4800, 520],
      "onError": "continueRegularOutput",
      "parameters": {
        "authentication": "accessToken",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "n8n-workflows"
        },
        "text": "=:handshake:  *LinkedIn outreach ready — send manually*\n\n*{{ $('Build LinkedIn Outreach').item.json.person_name }}*{{ [$('Build LinkedIn Outreach').item.json.person_title, $('Build LinkedIn Outreach').item.json.company_name].filter(Boolean).length ? '  ·  ' + [$('Build LinkedIn Outreach').item.json.person_title, $('Build LinkedIn Outreach').item.json.company_name].filter(Boolean).join(' @ ') : '' }}\n\n{{ $('Build LinkedIn Outreach').item.json.linkedin_line }}\n\n{{ $('Build LinkedIn Outreach').item.json.signal ? ':dart:  ' + $('Build LinkedIn Outreach').item.json.signal : '' }}\n{{ $('Build LinkedIn Outreach').item.json.warning_line }}\n\n:speech_balloon:  *_Intro_*\n```\n{{ ($('Build LinkedIn Outreach').item.json.intro_text || '').slice(0, 700) }}{{ ($('Build LinkedIn Outreach').item.json.intro_text || '').length > 700 ? '…' : '' }}\n```\n{{ $('Build LinkedIn Outreach').item.json.followups_count ? ':arrow_down:  *_Follow-ups · ' + $('Build LinkedIn Outreach').item.json.followups_count + ' step' + ($('Build LinkedIn Outreach').item.json.followups_count == 1 ? '' : 's') + '_*' : '' }}\n{{ $('Build LinkedIn Outreach').item.json.followups_count ? '```\\n' + ($('Build LinkedIn Outreach').item.json.followups_preview || '').slice(0, 1200) + (($('Build LinkedIn Outreach').item.json.followups_preview || '').length > 1200 ? '…' : '') + '\\n```' : '' }}\n\n<{{ $('Build LinkedIn Outreach').item.json.link }}|Open lead in Twain>",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "unfurl_links": false,
          "unfurl_media": false
        }
      }
    },
    {
      "id": "slackTwainError",
      "name": "Slack: Twain error",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [4420, 740],
      "onError": "continueRegularOutput",
      "parameters": {
        "authentication": "accessToken",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "n8n-workflows"
        },
        "text": "=:rotating_light:  *Twain Sequence generation failed*\n\n*{{ $('Normalize LinkedIn Lead').item.json.name || $('Normalize LinkedIn Lead').item.json.email || 'Unknown lead' }}*{{ $('Normalize LinkedIn Lead').item.json.company ? '  ·  ' + $('Normalize LinkedIn Lead').item.json.company : '' }}\n\n*Error:*\n{{ String($json.error?.message ?? $json.message ?? 'Unknown error').replace(/^\\s*\\d{3}\\s*-\\s*/, '').trim() }}\n\n<https://www.twain.ai/w|Open Twain workspace>",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "unfurl_links": false,
          "unfurl_media": false
        }
      }
    },
    {
      "id": "slackSkippedNoLinkedin",
      "name": "Slack: Skipped (no LinkedIn)",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [4040, 740],
      "onError": "continueRegularOutput",
      "parameters": {
        "authentication": "accessToken",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "n8n-workflows"
        },
        "text": "=:no_entry_sign:  *Twain — LinkedIn Signal Outreach — skipped*\n\n*{{ $('Normalize LinkedIn Lead').item.json.name || $('Normalize LinkedIn Lead').item.json.email || 'Unknown lead' }}*{{ $('Normalize LinkedIn Lead').item.json.company ? '  ·  ' + $('Normalize LinkedIn Lead').item.json.company : '' }}\n\nThe LinkedIn lookup didn't return a profile URL for this lead, so there's no one to reach out to on LinkedIn. Enable a LinkedIn Lookup provider (or add a LinkedIn URL upstream) to catch leads like this.\n\n<https://www.twain.ai/w|Open Twain workspace>",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "unfurl_links": false,
          "unfurl_media": false
        }
      }
    },
    {
      "id": "scheduleHubspotSegmentPoll",
      "name": "Schedule: HubSpot segment poll",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [240, 690],
      "disabled": true,
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      }
    },
    {
      "id": "hubspotSegmentMembers",
      "name": "HubSpot: Segment members",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [620, 520],
      "disabled": true,
      "parameters": {
        "url": "https://api.hubapi.com/contacts/v1/lists/REPLACE_WITH_HUBSPOT_LIST_ID/contacts/recent",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotOAuth2Api",
        "options": {
          "timeout": 30000
        }
      }
    },
    {
      "id": "pushToHeyreach",
      "name": "Push to HeyReach",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [4800, 740],
      "disabled": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "https://api.heyreach.io/api/public/campaign/AddLeadsToCampaignV2",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {\n  campaignId: Number('REPLACE_WITH_HEYREACH_CAMPAIGN_ID') || 'REPLACE_WITH_HEYREACH_CAMPAIGN_ID',\n  accountLeadPairs: [{\n    lead: {\n      profileUrl: $('Build LinkedIn Outreach').item.json.linkedin_url,\n      firstName: ($('Build LinkedIn Outreach').item.json.person_name || \"\").trim().split(\" \")[0] || \"\",\n      lastName: ($('Build LinkedIn Outreach').item.json.person_name || \"\").trim().split(\" \").slice(1).join(\" \") || \"\",\n      companyName: $('Build LinkedIn Outreach').item.json.company_name || \"\",\n      customUserFields: [{ name: \"twain_intro\", value: $('Build LinkedIn Outreach').item.json.intro_text || \"\" }]\n    }\n  }]\n} }}",
        "options": {
          "timeout": 60000
        }
      }
    }
  ],
  "connections": {
    "Signal Webhook": {
      "main": [
        [
          {
            "node": "LinkedIn Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Settings": {
      "main": [
        [
          {
            "node": "Normalize LinkedIn Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize LinkedIn Lead": {
      "main": [
        [
          {
            "node": "Dedupe Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dedupe Leads": {
      "main": [
        [
          {
            "node": "Needs LinkedIn Lookup?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs LinkedIn Lookup?": {
      "main": [
        [
          {
            "node": "LinkedIn Lookup — Findymail",
            "type": "main",
            "index": 0
          },
          {
            "node": "LinkedIn Lookup — Apollo",
            "type": "main",
            "index": 0
          },
          {
            "node": "LinkedIn Lookup — People Data Labs",
            "type": "main",
            "index": 0
          },
          {
            "node": "LinkedIn Lookup — Proxycurl",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Resolved Contact",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "LinkedIn Lookup — Findymail": {
      "main": [
        [
          {
            "node": "Apply Resolved LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Lookup — Apollo": {
      "main": [
        [
          {
            "node": "Apply Resolved LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Lookup — People Data Labs": {
      "main": [
        [
          {
            "node": "Apply Resolved LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Lookup — Proxycurl": {
      "main": [
        [
          {
            "node": "Apply Resolved LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Resolved LinkedIn": {
      "main": [
        [
          {
            "node": "Resolved Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolved Contact": {
      "main": [
        [
          {
            "node": "Has LinkedIn URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has LinkedIn URL?": {
      "main": [
        [
          {
            "node": "Twain Generate Sequence",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Skipped (no LinkedIn)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Twain Generate Sequence": {
      "main": [
        [
          {
            "node": "Build LinkedIn Outreach",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Twain error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build LinkedIn Outreach": {
      "main": [
        [
          {
            "node": "Slack: LinkedIn outreach ready",
            "type": "main",
            "index": 0
          },
          {
            "node": "Push to HeyReach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: HubSpot segment poll": {
      "main": [
        [
          {
            "node": "HubSpot: Segment members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot: Segment members": {
      "main": [
        [
          {
            "node": "LinkedIn Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

What you add after import:

  • Twain Header Auth — your API key as an n8n Header Auth credential (X-Api-Key).
  • Real campaign_id — paste your LinkedIn intro + follow-up campaign into the LinkedIn Settings node, replacing REPLACE_WITH_LINKEDIN_CAMPAIGN_ID. One labeled field, no digging in code.
  • Slack — the channel the outreach draft lands in.
  • Optional source or sender — wire HubSpot only if a segment drives the workflow, and HeyReach only if you push approved copy to a LinkedIn sender. Both ship disabled.

That's it. From the next signal on, you get a researched, personalized LinkedIn intro and follow-ups in your Slack, drafted and waiting — and you decide what goes out.