{
  "id": "tqW6fqm049FBLk47",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "Draft personalized LinkedIn outreach from buying signals with Twain",
  "tags": [],
  "nodes": [
    {
      "id": "5e7ff7cd-5089-4ecb-87fe-2273f338aece",
      "name": "Signal Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        528
      ],
      "parameters": {
        "path": "linkedin-signal",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "6c4d49cd-5da2-4688-ab0b-d6301010eb01",
      "name": "LinkedIn Settings",
      "type": "n8n-nodes-base.set",
      "position": [
        1008,
        528
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "campaign_id",
              "name": "campaign_id",
              "type": "string",
              "value": "REPLACE_WITH_LINKEDIN_CAMPAIGN_ID"
            },
            {
              "id": "heyreach_campaign_id",
              "name": "heyreach_campaign_id",
              "type": "string",
              "value": "REPLACE_WITH_HEYREACH_CAMPAIGN_ID"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "3ef77793-fff5-4674-8b95-c4335165692e",
      "name": "Normalize LinkedIn Lead",
      "type": "n8n-nodes-base.code",
      "position": [
        1392,
        528
      ],
      "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};"
      },
      "typeVersion": 2
    },
    {
      "id": "14e82d39-0a91-4be7-9164-2185a1551fbb",
      "name": "Dedupe Leads",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        1760,
        528
      ],
      "parameters": {
        "options": {
          "scope": "node",
          "historySize": 100000
        },
        "operation": "removeItemsSeenInPreviousExecutions",
        "dedupeValue": "={{ $json.uid }}"
      },
      "typeVersion": 2
    },
    {
      "id": "3391849c-e04c-488a-9cd8-00f63eadc98d",
      "name": "Has LinkedIn URL?",
      "type": "n8n-nodes-base.if",
      "position": [
        3664,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-li-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.has_linkedin_url }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "0057d90d-80a6-4bf4-afed-137f847858c6",
      "name": "Twain Generate Sequence",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueErrorOutput",
      "maxTries": 3,
      "position": [
        4048,
        528
      ],
      "parameters": {
        "url": "https://public.api.twain.ai/v2/Generate/Sequence",
        "method": "POST",
        "options": {
          "timeout": 180000
        },
        "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} }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.4,
      "waitBetweenTries": 5000
    },
    {
      "id": "02b1b346-a8df-4eec-ab71-59efa404b28a",
      "name": "Build LinkedIn Outreach",
      "type": "n8n-nodes-base.code",
      "position": [
        4432,
        528
      ],
      "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 ? \"  \u2014  \" + 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};"
      },
      "typeVersion": 2
    },
    {
      "id": "bf4d6fc4-7785-489c-9d5c-a3881fae4e90",
      "name": "Slack: LinkedIn outreach ready",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        4800,
        528
      ],
      "parameters": {
        "text": "=*LinkedIn outreach ready \u2014 send manually*\n*{{ $('Build LinkedIn Outreach').item.json.person_name }}*{{ $('Build LinkedIn Outreach').item.json.person_title ? '  \u00b7  ' + $('Build LinkedIn Outreach').item.json.person_title : '' }}{{ $('Build LinkedIn Outreach').item.json.company_name ? '  \u00b7  ' + $('Build LinkedIn Outreach').item.json.company_name : '' }}\n{{ $('Build LinkedIn Outreach').item.json.linkedin_url ? '<' + $('Build LinkedIn Outreach').item.json.linkedin_url + '|LinkedIn profile>\\n' : '' }}{{ $('Build LinkedIn Outreach').item.json.signal ? $('Build LinkedIn Outreach').item.json.signal + '\\n' : '' }}{{ $('Build LinkedIn Outreach').item.json.warning_line ? $('Build LinkedIn Outreach').item.json.warning_line + '\\n' : '' }}```\n{{ $('Build LinkedIn Outreach').item.json.intro_text }}\n```{{ $('Build LinkedIn Outreach').item.json.followups_count ? '\\n*Follow-ups (' + $('Build LinkedIn Outreach').item.json.followups_count + ')*\\n```\\n' + $('Build LinkedIn Outreach').item.json.followups_preview + '\\n```' : '' }}\n<{{ $('Build LinkedIn Outreach').item.json.link }}|Open lead in Twain>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "n8n-workflows"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "d23352c8-d2ea-4489-92af-2681f47bdfda",
      "name": "Slack: Twain error",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        4432,
        752
      ],
      "parameters": {
        "text": "=*Twain sequence generation failed*\n*{{ $('Normalize LinkedIn Lead').item.json.name || $('Normalize LinkedIn Lead').item.json.email || 'Unknown lead' }}*{{ $('Normalize LinkedIn Lead').item.json.company ? '  \u00b7  ' + $('Normalize LinkedIn Lead').item.json.company : '' }}\n{{ String($json.error?.message ?? $json.message ?? 'Unknown error').replace(/^\\s*\\d{3}\\s*-\\s*/, '').trim() }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "n8n-workflows"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "d7eb0d22-6868-4e0c-a69c-50f8f91552dc",
      "name": "Slack: Skipped (no LinkedIn)",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        4048,
        752
      ],
      "parameters": {
        "text": "=*Skipped \u2014 no LinkedIn URL*\n*{{ $('Normalize LinkedIn Lead').item.json.name || $('Normalize LinkedIn Lead').item.json.email || 'Unknown lead' }}*{{ $('Normalize LinkedIn Lead').item.json.company ? '  \u00b7  ' + $('Normalize LinkedIn Lead').item.json.company : '' }}\nNo profile URL resolved \u2014 add one upstream or enable a lookup provider.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "n8n-workflows"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "d736051c-3928-4862-9eb4-bfef7bdce616",
      "name": "Schedule: HubSpot segment poll",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        240,
        704
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "a8dc0e7a-744b-405b-b7ac-f416f90aea39",
      "name": "HubSpot: List memberships",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        448,
        704
      ],
      "parameters": {
        "url": "https://api.hubapi.com/crm/v3/lists/REPLACE_WITH_HUBSPOT_LIST_ID/memberships?limit=100",
        "options": {
          "timeout": 30000
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "hubspotApi": {
          "name": "<your credential>"
        },
        "httpBearerAuth": {
          "name": "<your credential>"
        },
        "hubspotOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "3be7b848-7a0c-47e7-9570-3c893c0b4e23",
      "name": "Push to HeyReach",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        4800,
        752
      ],
      "parameters": {
        "url": "https://api.heyreach.io/api/public/campaign/AddLeadsToCampaignV2",
        "method": "POST",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={{ {\n  campaignId: Number($('LinkedIn Settings').first().json.heyreach_campaign_id) || $('LinkedIn Settings').first().json.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_message\", value: $('Build LinkedIn Outreach').item.json.intro_text || \"\" }]\n    }\n  }]\n} }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7256dddd-4562-47d9-810c-7ad8df577294",
      "name": "Needs LinkedIn Lookup?",
      "type": "n8n-nodes-base.if",
      "position": [
        2144,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "needs-li-1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ !$('Normalize LinkedIn Lead').item.json.has_linkedin_url && Boolean($('Normalize LinkedIn Lead').item.json.email) }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "3ff02443-b2b6-47eb-aa02-8a3c86fd7d0a",
      "name": "LinkedIn Lookup \u2014 Findymail",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        2528,
        304
      ],
      "parameters": {
        "url": "https://app.findymail.com/api/search/reverse-email",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "jsonBody": "={{ { email: $('Normalize LinkedIn Lead').item.json.email, with_profile: true } }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "a988ae64-30cc-4efd-bc4a-a7281a8759d2",
      "name": "Apply Resolved LinkedIn",
      "type": "n8n-nodes-base.code",
      "position": [
        2912,
        528
      ],
      "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"
      },
      "typeVersion": 2
    },
    {
      "id": "c5b04acf-05bf-4f0a-aa5b-812a78becbbe",
      "name": "Resolved Contact",
      "type": "n8n-nodes-base.merge",
      "position": [
        3280,
        528
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "d7e43ed3-d177-496b-9ca6-4b24bc250746",
      "name": "Sticky: 3 \u00b7 Resolve LinkedIn (optional)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2736,
        -368
      ],
      "parameters": {
        "color": 7,
        "width": 644,
        "height": 390,
        "content": "## LinkedIn lookup (optional)\nResolves a LinkedIn URL from the email when a lead has none, then feeds it to Twain. The whole block is DISABLED by default.\nTo turn it on: pick ONE provider node below, add its API-key credential (Header Auth), adjust the response field in `Apply Resolved LinkedIn` if needed, and ENABLE that one node.\nNodes are pre-configured with each provider's API URL/auth/body:\n\u2022 Findymail (default) \u2014 POST reverse-email \u00b7 `Authorization: Bearer <API key>`\n\u2022 Apollo \u2014 POST people/match \u00b7 `X-Api-Key`\n\u2022 People Data Labs \u2014 GET person/enrich \u00b7 `X-Api-Key`\n\u2022 Proxycurl \u2014 GET resolve/email \u00b7 `Authorization: Bearer`\nTwain also resolves LinkedIn from the email during research; this is for resolving it BEFORE Twain or with your own provider."
      },
      "typeVersion": 1
    },
    {
      "id": "339779b1-085e-4767-8545-17232ddf0725",
      "name": "LinkedIn Lookup \u2014 Apollo",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "disabled": true,
      "position": [
        2528,
        80
      ],
      "parameters": {
        "url": "https://api.apollo.io/api/v1/people/match",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "jsonBody": "={{ { email: $('Normalize LinkedIn Lead').item.json.email } }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "83174fca-b188-40dd-ab0d-f1582fccce83",
      "name": "LinkedIn Lookup \u2014 People Data Labs",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "disabled": true,
      "position": [
        2528,
        -144
      ],
      "parameters": {
        "url": "=https://api.peopledatalabs.com/v5/person/enrich?email={{ encodeURIComponent($('Normalize LinkedIn Lead').item.json.email) }}",
        "options": {
          "timeout": 30000
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "7aaf56e4-c6ef-4028-adae-021f5e34f123",
      "name": "LinkedIn Lookup \u2014 Proxycurl",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "disabled": true,
      "position": [
        2528,
        -368
      ],
      "parameters": {
        "url": "=https://nubela.co/proxycurl/api/linkedin/profile/resolve/email?email={{ encodeURIComponent($('Normalize LinkedIn Lead').item.json.email) }}",
        "options": {
          "timeout": 30000
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "b8aa2230-ed8f-4441-bce2-f636bc359068",
      "name": "HubSpot: Read contacts",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        688,
        704
      ],
      "parameters": {
        "url": "https://api.hubapi.com/crm/v3/objects/contacts/batch/read",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "jsonBody": "={{ { properties: [\"email\",\"firstname\",\"lastname\",\"company\",\"jobtitle\",\"hs_linkedin_url\"], inputs: ($('HubSpot: List memberships').first().json.results || []).map(r => ({ id: r.recordId })) } }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "38241ddc-4462-45d7-ab6e-968fa2117c54",
      "name": "Map HubSpot Contacts",
      "type": "n8n-nodes-base.code",
      "position": [
        928,
        704
      ],
      "parameters": {
        "jsCode": "const res = $json || {};\nconst results = Array.isArray(res.results) ? res.results : [];\nreturn results.map((r) => {\n  const p = r.properties || {};\n  const name = [p.firstname, p.lastname].filter(Boolean).join(' ').trim();\n  return { json: {\n    email: (p.email || '').toLowerCase(),\n    name: name,\n    company: p.company || '',\n    linkedin_url: p.hs_linkedin_url || '',\n    signal: 'Entered HubSpot list',\n    hubspot_id: r.id\n  } };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "83b57b7b-d651-4bd4-9414-23a5fa2a041f",
      "name": "Sticky: What this does",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        -432
      ],
      "parameters": {
        "width": 848,
        "height": 692,
        "content": "## LinkedIn Signal Outreach \u2014 a researched, personalized draft the moment a lead is in-market\n\nWhen a buying signal fires (a form, a HubSpot list, a webhook), this workflow turns it into a **ready-to-send LinkedIn intro + follow-ups** in your Slack about 20 seconds later \u2014 you review and send from your own profile. It never auto-sends.\n\n### Why Twain\nThe research is what makes outreach land and what doesn't scale \u2014 10+ hours of profile-reading for a week of leads, so it gets skipped and the copy gets generic. Twain's campaign **Agent** reads each person and their company and writes the message on a real angle, from a brief you set once in plain language. Same research and personalization a good rep would do by hand, in seconds, at list scale.\n\n### What it does\n\u2022 Normalizes the signal, dedupes, and (optionally) resolves a LinkedIn URL from the email\n\u2022 Calls Twain to research the lead and draft the LinkedIn intro + follow-ups\n\u2022 Posts the draft to Slack for a human to send \u2014 optionally stages it in a HeyReach campaign\n\n### How to set up\n1. Twain account + a LinkedIn intro/follow-up campaign at **https://www.twain.ai** \u2014 its Agent writes the copy.\n2. Paste the `campaign_id` into **LinkedIn Settings**; add the Twain (Header Auth `X-Api-Key`) + Slack credentials.\n3. Point your signal source at the webhook. Generate/Sequence costs 1 credit per lead.\n\nBuilt with Twain \u2014 https://www.twain.ai"
      },
      "typeVersion": 1
    },
    {
      "id": "090049c4-31b3-417b-accc-7a5db9c8c10d",
      "name": "Sticky: 1 \u00b7 Signal intake & setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 1150,
        "height": 612,
        "content": "### 1 \u00b7 Signal intake & setup\nA buying signal hits the **webhook** (a form, a HubSpot list, any tool that means \"reach out now\"). Set your Twain campaign in **LinkedIn Settings** (`campaign_id`) \u2014 its Agent writes the copy.\n**Wire:** Twain Header Auth (`X-Api-Key`) + Slack. Webhook auth starts at `none`; add a shared secret before going live. Generate/Sequence = 1 credit per lead."
      },
      "typeVersion": 1
    },
    {
      "id": "301e45e5-a258-443b-980e-44bd68a84656",
      "name": "Sticky: 2 \u00b7 Normalize & dedupe",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 596,
        "content": "### 2 \u00b7 Normalize & dedupe\nParse the signal into a clean lead, then skip anyone already processed (dedupe by a stable id). Reruns are safe."
      },
      "typeVersion": 1
    },
    {
      "id": "5c3dc3e0-c2bb-4d7f-ad44-0a4432776c29",
      "name": "Sticky: 4 \u00b7 Generate with Twain",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3552,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 700,
        "height": 692,
        "content": "### 4 \u00b7 Generate with Twain\nTwain researches the lead and drafts the LinkedIn intro + follow-ups from your campaign's Agent. Retries handle 429s. For research-heavy volume, switch to `POST /v2/Generate/Contact/Async` with a callback so a cold-lead generation can't hit the 60s gateway timeout."
      },
      "typeVersion": 1
    },
    {
      "id": "9210a4b4-0ad9-4fa6-bada-b615efe631b6",
      "name": "Sticky: 5 \u00b7 Review & route",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4304,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 820,
        "height": 744,
        "content": "### 5 \u00b7 Review & route\nThe draft is posted to **Slack for a human to send** \u2014 this template never auto-sends.\n\u2022 Free LinkedIn: message 1st-degree connections + limited invites. Cold non-connections need InMail (Sales Navigator).\n\u2022 n8n has no native LinkedIn DM node, so sending stays manual.\n\nOptional (disabled): **Push to HeyReach** stages the copy in a HeyReach campaign \u2014 set `heyreach_campaign_id` in *LinkedIn Settings*. Runs through your LinkedIn account; ToS/ban risk, Sales Navigator recommended."
      },
      "typeVersion": 1
    },
    {
      "id": "b2a8dabc-7564-4e06-8376-ddd7361a8693",
      "name": "Sticky: HubSpot segment source (optional)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        912
      ],
      "parameters": {
        "color": 7,
        "width": 980,
        "height": 300,
        "content": "### HubSpot segment source (optional \u00b7 disabled)\nDrive this from HubSpot instead of the webhook: a contact entering a dynamic list is the signal. Enable *Schedule \u2192 List memberships (v3) \u2192 Read contacts \u2192 Map \u2192 LinkedIn Settings*.\nAuth: a HubSpot **Service Key** / private-app token in a **Bearer Auth** credential \u2014 not OAuth2 (n8n blocks it in HTTP nodes). Scopes: `crm.lists.read` + `crm.objects.contacts.read`. Set your list id (replace `REPLACE_WITH_HUBSPOT_LIST_ID`). Keep disabled until configured."
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "27de602a-24cc-4439-ad1d-a7bfc0f56216",
  "nodeGroups": [],
  "connections": {
    "Dedupe Leads": {
      "main": [
        [
          {
            "node": "Needs LinkedIn Lookup?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Signal Webhook": {
      "main": [
        [
          {
            "node": "LinkedIn Settings",
            "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
          }
        ]
      ]
    },
    "LinkedIn Settings": {
      "main": [
        [
          {
            "node": "Normalize LinkedIn Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map HubSpot Contacts": {
      "main": [
        [
          {
            "node": "LinkedIn Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot: Read contacts": {
      "main": [
        [
          {
            "node": "Map HubSpot Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs LinkedIn Lookup?": {
      "main": [
        [
          {
            "node": "LinkedIn Lookup \u2014 Findymail",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Resolved Contact",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Apply Resolved LinkedIn": {
      "main": [
        [
          {
            "node": "Resolved Contact",
            "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
          }
        ]
      ]
    },
    "Normalize LinkedIn Lead": {
      "main": [
        [
          {
            "node": "Dedupe Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Twain Generate Sequence": {
      "main": [
        [
          {
            "node": "Build LinkedIn Outreach",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Twain error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot: List memberships": {
      "main": [
        [
          {
            "node": "HubSpot: Read contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Lookup \u2014 Findymail": {
      "main": [
        [
          {
            "node": "Apply Resolved LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: HubSpot segment poll": {
      "main": [
        [
          {
            "node": "HubSpot: List memberships",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}