AutomationFlowsSocial Media › Draft Personalized Linkedin Outreach From Buying Signals with Twain and Slack

Draft Personalized Linkedin Outreach From Buying Signals with Twain and Slack

ByMohamed Yahya Chahin @motwain on n8n.io

This workflow captures buying-signal leads via a webhook (or optionally polls a HubSpot list), generates a personalized LinkedIn intro and follow-up sequence with Twain, and posts the ready-to-send draft to Slack, with an optional step to stage the lead in a HeyReach campaign.…

Webhook trigger★★★★☆ complexity29 nodesHTTP RequestSlack
Social Media Trigger: Webhook Nodes: 29 Complexity: ★★★★☆ Added:
Draft Personalized Linkedin Outreach From Buying Signals with Twain and Slack — n8n workflow card showing HTTP Request, Slack integration

This workflow corresponds to n8n.io template #17193 — we link there as the canonical source.

This workflow follows the HTTP Request → Slack recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow captures buying-signal leads via a webhook (or optionally polls a HubSpot list), generates a personalized LinkedIn intro and follow-up sequence with Twain, and posts the ready-to-send draft to Slack, with an optional step to stage the lead in a HeyReach campaign.…

Source: https://n8n.io/workflows/17193/ — original creator credit. Request a take-down →

More Social Media workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Social Media

Automate your entire Instagram carousel publishing pipeline from a single webhook call. This workflow receives a product collection payload, loops through each slide image, uploads every asset via Upl

HTTP Request, N8N Nodes Uploadtourl, Slack
Social Media

📦 Automated Instagram Product Drop via uploadtourl

HTTP Request, Airtable, Slack +1
Social Media

This workflow automates the post-publish process for YouTube videos, combining advanced SEO optimization, cross-platform promotion, and analytics reporting. It is designed for creators, marketers, and

YouTube, HTTP Request, LinkedIn +6
Social Media

generación_publicaciones (workflow 3) webhook all platforms decrypt publicar git. Uses dataTable, linkedIn, httpRequest. Webhook trigger; 50 nodes.

Data Table, LinkedIn, HTTP Request
Social Media

This template is ideal for sales teams, recruiters, business development professionals, and relationship managers who need to monitor changes in their network's LinkedIn profiles. Perfect for agencies

Google Sheets, HTTP Request, Slack