{
  "id": "k9fWvQnQXpPQ4CBU",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "Draft personalized welcome emails for new signups with Twain",
  "tags": [
    {
      "id": "wzeP6rYkNyfX1gKe",
      "name": "Welcome message",
      "createdAt": "2026-07-13T09:44:49.851Z",
      "updatedAt": "2026-07-13T09:44:49.851Z"
    }
  ],
  "nodes": [
    {
      "id": "a9ecce14-53cd-4ab9-b3ef-aa1a22dc55b2",
      "name": "Signup Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        240,
        352
      ],
      "parameters": {
        "path": "welcome-signup",
        "options": {},
        "httpMethod": "POST",
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "dac1b12b-f538-4383-a42d-02b0f80b50a3",
      "name": "Normalize Signup",
      "type": "n8n-nodes-base.set",
      "position": [
        1008,
        528
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "uid",
              "name": "uid",
              "type": "string",
              "value": "={{ $json.body?.uid ?? $json.uid }}"
            },
            {
              "id": "email",
              "name": "email",
              "type": "string",
              "value": "={{ ($json.body?.email ?? $json.email ?? \"\").toLowerCase().trim() }}"
            },
            {
              "id": "displayName",
              "name": "displayName",
              "type": "string",
              "value": "={{ $json.body?.displayName ?? $json.displayName ?? \"\" }}"
            },
            {
              "id": "signup_ts",
              "name": "signup_ts",
              "type": "string",
              "value": "={{ $json.body?.signup_ts ?? $json.signup_ts ?? $now.toISO() }}"
            },
            {
              "id": "campaign_id",
              "name": "campaign_id",
              "type": "string",
              "value": "REPLACE_WITH_WELCOME_CAMPAIGN_ID"
            },
            {
              "id": "linkedin_url",
              "name": "linkedin_url",
              "type": "string",
              "value": "={{ ($json.body?.linkedin_url ?? $json.body?.linkedin_profile_url ?? $json.linkedin_url ?? $json.linkedin_profile_url ?? \"\").trim() }}"
            },
            {
              "id": "email_domain",
              "name": "email_domain",
              "type": "string",
              "value": "={{ ((($json.body?.email ?? $json.email ?? \"\").toLowerCase().trim()).split(\"@\")[1] ?? \"\").toLowerCase() }}"
            },
            {
              "id": "is_personal_email",
              "name": "is_personal_email",
              "type": "boolean",
              "value": "={{ [\"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\"].includes(((($json.body?.email ?? $json.email ?? \"\").toLowerCase().trim()).split(\"@\")[1] ?? \"\").toLowerCase()) }}"
            },
            {
              "id": "has_linkedin_url",
              "name": "has_linkedin_url",
              "type": "boolean",
              "value": "={{ Boolean(($json.body?.linkedin_url ?? $json.body?.linkedin_profile_url ?? $json.linkedin_url ?? $json.linkedin_profile_url ?? \"\").trim()) }}"
            },
            {
              "id": "has_work_email",
              "name": "has_work_email",
              "type": "boolean",
              "value": "={{ Boolean((($json.body?.email ?? $json.email ?? \"\").toLowerCase().trim())) && ![\"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\"].includes((((($json.body?.email ?? $json.email ?? \"\").toLowerCase().trim()).split(\"@\")[1] ?? \"\").toLowerCase())) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e7710068-3b23-41ac-b67c-a63a38895fae",
      "name": "Dedupe New Signups",
      "type": "n8n-nodes-base.removeDuplicates",
      "position": [
        1760,
        528
      ],
      "parameters": {
        "options": {
          "scope": "node",
          "historySize": 100000
        },
        "operation": "removeItemsSeenInPreviousExecutions",
        "dedupeValue": "={{ $json.uid }}"
      },
      "typeVersion": 2
    },
    {
      "id": "6520ceb0-3c8a-4f0e-9eac-c790cb9554af",
      "name": "Has usable contact?",
      "type": "n8n-nodes-base.if",
      "position": [
        3664,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "usable-1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ Boolean($json.has_work_email) || Boolean($json.has_linkedin_url) }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "54a956e5-ecbe-4d5b-b5be-dc1c4bae97f3",
      "name": "Slack: Skipped (cannot personalize)",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        4048,
        752
      ],
      "parameters": {
        "text": "=:no_entry_sign:  *Twain \u2014 Personalized Welcome Draft \u2014 skipped*\n*{{ $('Normalize Signup').item.json.displayName || $('Normalize Signup').item.json.email || 'Unknown signup' }}*\n{{ $('Normalize Signup').item.json.email || 'No email provided' }}\nNo work email, and the LinkedIn lookup didn't return a profile URL for this signup \u2014 so there's no one to personalize a welcome for. Enable a LinkedIn Lookup provider (or add a work email / LinkedIn URL upstream) to catch leads like this.\n<https://www.twain.ai/w|Open Twain workspace>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Channel Settings').first().json.slack_channel }}"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "7ebf6cbb-cb80-429f-97d6-46c2e609f28e",
      "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 Signup').item.json.campaign_id,\n  contact: {\n    ...($('Normalize Signup').item.json.has_work_email ? { work_email: $('Normalize Signup').item.json.email } : {}),\n    ...($json.has_linkedin_url ? { linkedin_profile_url: $json.linkedin_url } : {}),\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": "9d4ccb2d-633f-4a4e-ae7a-90fcab334f5e",
      "name": "Build Welcome Draft",
      "type": "n8n-nodes-base.code",
      "position": [
        4432,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const api = $json;\nconst norm = $(\"Normalize Signup\").item.json;\nconst cfg = $(\"Channel Settings\").item.json;\nconst messages = Array.isArray(api.messages) ? api.messages : [];\n\n// --- Parse configurable step->channel split (1-based indices) ---\nconst parseSteps = (s) =>\n  String(s ?? \"\")\n    .split(\",\")\n    .map((x) => parseInt(String(x).trim(), 10))\n    .filter((n) => Number.isInteger(n) && n >= 1);\nlet emailSteps = parseSteps(cfg.email_steps);\nconst linkedinSteps = parseSteps(cfg.linkedin_steps);\n// Fall back to step 1 for email if none configured.\nif (emailSteps.length === 0) emailSteps = [1];\n\nconst stepMsg = (i1) => messages[i1 - 1] || {}; // i1 is 1-based\n\n// --- Email step(s) ---\nconst primaryEmailIdx = emailSteps[0]; // 1-based\nconst primary = stepMsg(primaryEmailIdx);\nconst esc = (s) =>\n  String(s ?? \"\")\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\");\nconst toHtml = (s) => (s ? esc(s).split(\"\\n\").join(\"<br>\") : \"\");\n\nconst primarySubject = String(\n  primary.subject_line ||\n    api.subject_line ||\n    \"Welcome to Twain, \" + (norm.displayName || \"there\"),\n);\nconst primaryBody = String(primary.message || \"\");\n\nconst email_message = {\n  step: primaryEmailIdx,\n  subject: primarySubject,\n  body_text: primaryBody,\n  body_html: toHtml(primaryBody),\n};\n\n// Preview of ALL email steps (for Slack), escape-safe single string.\nconst email_preview =\n  emailSteps\n    .map((i1) => {\n      const m = stepMsg(i1);\n      const subj = m.subject_line ? \"Subject: \" + m.subject_line + \"\\n\" : \"\";\n      return subj + (m.message || \"\");\n    })\n    .filter(Boolean)\n    .join(\"\\n\\n--------\\n\\n\") || \"No email steps.\";\n\n// --- LinkedIn step(s) ---\nconst linkedin_messages = linkedinSteps.map((i1) => ({\n  step: i1,\n  message: String(stepMsg(i1).message || \"\"),\n}));\nconst has_linkedin_steps = linkedin_messages.length > 0;\nconst linkedin_preview =\n  linkedin_messages.map((m) => m.message).join(\"\\n\\n--------\\n\\n\") ||\n  \"No LinkedIn steps.\";\n\n// --- Full sequence preview (unchanged, all steps) ---\nconst sequencePreview =\n  messages\n    .map((m, i) => {\n      const subj = m.subject_line ? \"Subject: \" + m.subject_line + \"\\n\" : \"\";\n      return subj + (m.message || \"\");\n    })\n    .join(\"\\n\\n--------\\n\\n\") || \"No messages returned.\";\n\n// --- Research / contact context ---\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 linkedin_url = (person && person.linkedin_profile_url) || norm.linkedin_url || \"\";\nconst linkedin_line = linkedin_url ? \":link:  <\" + linkedin_url + \"|LinkedIn profile>\" : \"\";\n// Prebuilt escape-safe warnings line (single token for Slack cards; empty when no warnings).\nconst warning_line =\n  warnings && warnings.has_warnings\n    ? \":warning:  *Warnings:* \" +\n      ((categories || []).join(\", \") || \"flagged\") +\n      (warnings.summary ? \"  \u2014  \" + warnings.summary : \"\")\n    : \"\";\n\nreturn {\n  to_email: norm.email || \"\",\n  has_work_email: norm.has_work_email === true,\n  // Primary email step (drives the Gmail draft + email Slack card)\n  subject: email_message.subject,\n  body_html: email_message.body_html,\n  body_text: email_message.body_text,\n  email_message: email_message,\n  email_preview: email_preview,\n  email_steps: emailSteps,\n  // LinkedIn steps (drive HeyReach + LinkedIn Slack card)\n  linkedin_messages: linkedin_messages,\n  linkedin_preview: linkedin_preview,\n  has_linkedin_steps: has_linkedin_steps,\n  linkedin_steps: linkedinSteps,\n  heyreach_campaign_id: cfg.heyreach_campaign_id || \"\",\n  // Shared\n  sequence_preview: sequencePreview,\n  step_count: messages.length,\n  person_name: person.name || norm.displayName || norm.email || \"Unknown\",\n  person_title: person.title || \"\",\n  company_name: company.name || \"\",\n  linkedin_url: linkedin_url,\n  linkedin_line: linkedin_line,\n  link: api.link || \"\",\n  warnings_line: categories.length ? categories.join(\", \") : \"\",\n  warnings_summary: warnings.summary || \"\",\n  warning_line: warning_line,\n}\n;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a5b3a83d-78a7-46c6-8105-4fc3616e3a54",
      "name": "Has Work Email?",
      "type": "n8n-nodes-base.if",
      "position": [
        4800,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "hwe-1",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.has_work_email }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "c30a33d9-1653-42ea-b6da-22e5361466c9",
      "name": "Gmail: Create Welcome Draft",
      "type": "n8n-nodes-base.gmail",
      "onError": "continueErrorOutput",
      "position": [
        5184,
        528
      ],
      "parameters": {
        "message": "={{ $('Build Welcome Draft').item.json.body_html }}",
        "options": {
          "sendTo": "={{ $('Build Welcome Draft').item.json.to_email }}"
        },
        "subject": "={{ $('Build Welcome Draft').item.json.subject }}",
        "resource": "draft",
        "emailType": "html"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "dd200fcf-04e3-4890-b5b3-12d68ce2837c",
      "name": "Slack: Email draft ready",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        5568,
        608
      ],
      "parameters": {
        "text": "=*Welcome draft ready to review*\n*{{ $('Build Welcome Draft').item.json.person_name }}*{{ [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).length ? '  \u00b7  ' + [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).join(' @ ') : '' }}\n{{ $('Build Welcome Draft').item.json.to_email }}\n{{ $('Build Welcome Draft').item.json.linkedin_line }}\n{{ $('Build Welcome Draft').item.json.warning_line }}\n*_{{ $('Build Welcome Draft').item.json.subject }}_*\n```\n{{ ($('Build Welcome Draft').item.json.body_text || '').slice(0, 700) }}{{ ($('Build Welcome Draft').item.json.body_text || '').length > 700 ? '\u2026' : '' }}\n```\nSaved as a Gmail draft \u2014 open *Drafts* to review and send.\n<{{ $('Build Welcome Draft').item.json.link }}|Open lead in Twain>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Channel Settings').first().json.slack_channel }}"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "e468da70-63cf-4712-9545-abdb76e032ac",
      "name": "Slack: LinkedIn welcome ready",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        5184,
        752
      ],
      "parameters": {
        "text": "=*LinkedIn welcome ready \u2014 send manually*\n*{{ $('Build Welcome Draft').item.json.person_name }}*{{ [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).length ? '  \u00b7  ' + [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).join(' @ ') : '' }}\n{{ $('Build Welcome Draft').item.json.linkedin_line }}\n{{ $('Build Welcome Draft').item.json.warning_line }}\nNo work email on file \u2014 send this via LinkedIn.\n*_Sequence \u00b7 {{ $('Build Welcome Draft').item.json.step_count }} step{{ $('Build Welcome Draft').item.json.step_count == 1 ? '' : 's' }}_*\n```\n{{ ($('Build Welcome Draft').item.json.sequence_preview || '').slice(0, 1400) }}{{ ($('Build Welcome Draft').item.json.sequence_preview || '').length > 1400 ? '\u2026' : '' }}\n```\n<{{ $('Build Welcome Draft').item.json.link }}|Open lead in Twain>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Channel Settings').first().json.slack_channel }}"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "40aa933a-b458-494b-81cc-9a1af2c7c52e",
      "name": "Slack: Twain error",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        4432,
        752
      ],
      "parameters": {
        "text": "=:rotating_light:  *Twain Sequence generation failed*\n*{{ $('Normalize Signup').item.json.displayName || $('Normalize Signup').item.json.email || 'Unknown signup' }}*\n{{ $('Normalize Signup').item.json.email || 'No email provided' }}\n*Error:*\n{{ String($json.error?.message ?? $json.message ?? 'Unknown error').replace(/^\\s*\\d{3}\\s*-\\s*/, '').trim() }}\n<https://www.twain.ai/w|Open Twain workspace>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Channel Settings').first().json.slack_channel }}"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "5ad9d860-deb0-4c08-805f-4b7c33c95aec",
      "name": "Schedule: BigQuery backfill",
      "type": "n8n-nodes-base.scheduleTrigger",
      "disabled": true,
      "position": [
        240,
        560
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "e3b85d9e-526e-41fc-b999-470587894488",
      "name": "BigQuery: New signups",
      "type": "n8n-nodes-base.googleBigQuery",
      "disabled": true,
      "position": [
        624,
        416
      ],
      "parameters": {
        "options": {},
        "sqlQuery": "SELECT uid, email, display_name AS displayName, signup_ts\nFROM `your_dataset.signups`\nWHERE signup_ts > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)\nORDER BY signup_ts",
        "projectId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_GCP_PROJECT_ID"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 2.1
    },
    {
      "id": "db1f67e1-e636-43dc-8514-1e34a56719e3",
      "name": "Schedule: Firestore poll",
      "type": "n8n-nodes-base.scheduleTrigger",
      "disabled": true,
      "position": [
        240,
        752
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "9c83ff7c-10e2-483b-b610-f519f1822843",
      "name": "Firestore: New customers",
      "type": "n8n-nodes-base.googleFirebaseCloudFirestore",
      "disabled": true,
      "position": [
        624,
        640
      ],
      "parameters": {
        "query": "{\n  \"from\": [{ \"collectionId\": \"customers\" }],\n  \"orderBy\": [{ \"field\": { \"fieldPath\": \"created_at\" }, \"direction\": \"DESCENDING\" }],\n  \"limit\": 50\n}",
        "operation": "query",
        "projectId": "REPLACE_WITH_FIREBASE_PROJECT_ID",
        "authentication": "serviceAccount"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d664e839-8753-466c-bbac-7a5cf4993683",
      "name": "Sticky: Twain onboarding",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 304,
        "content": "## Twain onboarding (first time)\n1. Create an account & workspace at **https://www.twain.ai**\n2. Build a **new-user-welcome campaign** \u2014 its Agent decides the welcome copy + research depth (`research_rules`).\n3. Copy that **campaign_id** into *Normalize Signup* (replace `REPLACE_WITH_WELCOME_CAMPAIGN_ID`).\n4. Get an **API key** from workspace settings for the Header Auth credential.\n\nGenerate/Sequence costs 1 credit per signup.\n\n\ud83d\udcd6 **Full walkthrough:** https://www.twain.ai/blog/n8n-welcome-draft"
      },
      "typeVersion": 1
    },
    {
      "id": "f3581017-c5eb-4085-8529-cc76eae91154",
      "name": "Sticky: Credentials",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Credentials to wire\n- **Twain Generate Sequence** \u2192 Header Auth credential: Name `X-Api-Key`, Value your `twain-\u2026` key.\n- **Gmail: Create Welcome Draft** \u2192 Gmail OAuth2 (the founder mailbox where drafts land).\n- **Slack** nodes \u2192 Slack OAuth2 (auto-binds if one exists).\n- **Webhook** \u2192 enable Header Auth before going live (set a shared secret matched by your caller)."
      },
      "typeVersion": 1
    },
    {
      "id": "0f12cd20-0910-464a-a17f-0bfe6cbe8410",
      "name": "Slack: Draft not created",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        5568,
        832
      ],
      "parameters": {
        "text": "=*Welcome ready \u2014 no Gmail draft created*\n*{{ $('Build Welcome Draft').item.json.person_name }}*{{ [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).length ? '  \u00b7  ' + [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).join(' @ ') : '' }}\n{{ $('Build Welcome Draft').item.json.to_email }}\n{{ $('Build Welcome Draft').item.json.linkedin_line }}\n{{ $('Build Welcome Draft').item.json.warning_line }}\nGmail isn't connected (or the draft step failed), so nothing was saved to Drafts. Connect Gmail to auto-draft, or copy the message below to send manually.\n*_{{ $('Build Welcome Draft').item.json.subject }}_*\n```\n{{ ($('Build Welcome Draft').item.json.body_text || '').slice(0, 900) }}{{ ($('Build Welcome Draft').item.json.body_text || '').length > 900 ? '\u2026' : '' }}\n```\n<{{ $('Build Welcome Draft').item.json.link }}|Open lead in Twain>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Channel Settings').first().json.slack_channel }}"
        },
        "otherOptions": {
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "c6a473a8-39a1-457d-a14e-4a912dc1695a",
      "name": "Needs LinkedIn Lookup?",
      "type": "n8n-nodes-base.if",
      "position": [
        2144,
        528
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "needs-li-1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ !$('Normalize Signup').item.json.has_linkedin_url && Boolean($('Normalize Signup').item.json.email) }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "5919a455-0d50-4f31-9723-8fc6e5459004",
      "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 Signup').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": "7897dcd7-fab8-4170-b2fa-799efdc8e6ba",
      "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 Signup').item.json;\nreturn {\n  ...n,\n  linkedin_url: linkedin_url,\n  has_linkedin_url: Boolean(linkedin_url)\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5ce75f13-8405-4ca7-b7d3-5d297bd4b281",
      "name": "Resolved Contact",
      "type": "n8n-nodes-base.merge",
      "position": [
        3280,
        528
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "1449224c-d6c4-4592-89c8-03632ecbda41",
      "name": "Channel Settings",
      "type": "n8n-nodes-base.set",
      "position": [
        1392,
        528
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "email_steps",
              "name": "email_steps",
              "type": "string",
              "value": "1"
            },
            {
              "id": "linkedin_steps",
              "name": "linkedin_steps",
              "type": "string",
              "value": ""
            },
            {
              "id": "heyreach_campaign_id",
              "name": "heyreach_campaign_id",
              "type": "string",
              "value": "REPLACE_WITH_HEYREACH_CAMPAIGN_ID"
            },
            {
              "id": "slack_channel",
              "name": "slack_channel",
              "type": "string",
              "value": "welcome-signups"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d5683f20-f3e4-4869-9126-3815e1d1fd58",
      "name": "Has LinkedIn steps?",
      "type": "n8n-nodes-base.if",
      "position": [
        5952,
        752
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-li-steps-1",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Build Welcome Draft').item.json.has_linkedin_steps }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "aaed263d-83ea-4521-9cf6-029f660f6156",
      "name": "HeyReach: Add to Campaign",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "disabled": true,
      "position": [
        6320,
        752
      ],
      "parameters": {
        "url": "https://api.heyreach.io/api/public/campaign/AddLeadsToCampaignV2",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "jsonBody": "={{ {\n  campaignId: Number($('Build Welcome Draft').item.json.heyreach_campaign_id) || $('Build Welcome Draft').item.json.heyreach_campaign_id,\n  accountLeadPairs: [{\n    lead: {\n      profileUrl: $('Build Welcome Draft').item.json.linkedin_url,\n      firstName: ($('Build Welcome Draft').item.json.person_name || \"\").trim().split(\" \")[0] || \"\",\n      lastName: ($('Build Welcome Draft').item.json.person_name || \"\").trim().split(\" \").slice(1).join(\" \") || \"\",\n      companyName: $('Build Welcome Draft').item.json.company_name || \"\",\n      emailAddress: $('Build Welcome Draft').item.json.to_email || \"\",\n      customUserFields: [{ name: \"twain_message\", value: $('Build Welcome Draft').item.json.linkedin_preview || \"\" }]\n    }\n  }]\n} }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "1cf2c115-5471-4b75-85fd-69d0bd961521",
      "name": "Slack: LinkedIn steps queued (HeyReach)",
      "type": "n8n-nodes-base.slack",
      "position": [
        6704,
        752
      ],
      "parameters": {
        "text": "=*LinkedIn step(s) queued \u2014 HeyReach*\n*{{ $('Build Welcome Draft').item.json.person_name }}*{{ [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).length ? '  \u00b7  ' + [$('Build Welcome Draft').item.json.person_title, $('Build Welcome Draft').item.json.company_name].filter(Boolean).join(' @ ') : '' }}\n{{ $('Build Welcome Draft').item.json.linkedin_line }}\n{{ $('Build Welcome Draft').item.json.warning_line }}\n*_LinkedIn message_*\n```\n{{ ($('Build Welcome Draft').item.json.linkedin_preview || '').slice(0, 1400) }}{{ ($('Build Welcome Draft').item.json.linkedin_preview || '').length > 1400 ? '\u2026' : '' }}\n```\n{{ $('HeyReach: Add to Campaign').isExecuted ? ':white_check_mark:  Pushed to HeyReach campaign `' + ($('Build Welcome Draft').item.json.heyreach_campaign_id || '') + '`.' : ':wrench:  *Configure HeyReach to enable* \u2014 add the `X-API-KEY` credential + `heyreach_campaign_id`, then enable `HeyReach: Add to Campaign`. Until then, send these manually.' }}\n<{{ $('Build Welcome Draft').item.json.link }}|Open lead in Twain>",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Channel Settings').first().json.slack_channel }}"
        },
        "otherOptions": {
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $('Slack: Email draft ready').isExecuted ? $('Slack: Email draft ready').item.json.message_timestamp : ($('Slack: Draft not created').isExecuted ? $('Slack: Draft not created').item.json.message_timestamp : $('Slack: LinkedIn welcome ready').item.json.message_timestamp) }}"
            }
          },
          "unfurl_links": false,
          "unfurl_media": false,
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "fabef85e-7d22-44ef-a7ce-0e405de52044",
      "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 Signup').item.json.email } }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "3bd76e34-b31b-4930-b6b6-45be11a056f9",
      "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 Signup').item.json.email) }}",
        "options": {
          "timeout": 30000
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "78642433-b136-4e9a-bb8f-e86878ec6af8",
      "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 Signup').item.json.email) }}",
        "options": {
          "timeout": 30000
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "fa726d21-affd-4edb-b081-0a69a16ae3aa",
      "name": "Sticky: Try it out",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -816,
        -192
      ],
      "parameters": {
        "width": 920,
        "height": 1218,
        "content": "## Try It Out!\n\n**The moment someone signs up, this workflow automatically drafts a personalized welcome message for them** \u2014 researched and written by Twain, ready for a human to review and send. No more generic \"thanks for signing up.\"\n\nIt normalizes the signup, researches the person with **Twain** (resolving a LinkedIn URL first if needed), generates a welcome sequence, saves the email as a **Gmail draft**, and posts it to Slack for approval. LinkedIn-only signups get the message to send manually; optional LinkedIn steps can be queued in HeyReach. **Nothing is sent automatically.**\n\n### Who's it for\n\nFounder-led and PLG / sales teams who want every new signup to get a warm, personalized welcome touch \u2014 with zero manual writing.\n\n### How it works\n\n- **Signup comes in** \u2014 via webhook (or an optional BigQuery / Firestore backfill), normalized and de-duplicated by `uid`.\n- **Resolve contact** \u2014 personal-email signups get a LinkedIn / work-email lookup so there's a real person to research.\n- **Research + write** \u2014 Twain researches the person and generates a personalized welcome sequence.\n- **Draft & notify** \u2014 saves a Gmail draft (work email) or posts the message to Slack (LinkedIn-only); optional LinkedIn steps \u2192 HeyReach; everything posted to Slack for review.\n\n### How to set up\n\n- **Set `campaign_id`** in **Normalize Signup** to your Twain welcome campaign; set `slack_channel`, `email_steps` / `linkedin_steps`, and `heyreach_campaign_id` in **Channel Settings**.\n- **Add credentials:** Twain (HTTP Header Auth, header `X-Api-Key`), Gmail, and Slack \u2014 plus, if used, a LinkedIn-lookup provider and HeyReach.\n- **Trigger:** POST signups to the **Signup Webhook** as `{ email, displayName, linkedin_url, uid, signup_ts }`, or enable a backfill trigger.\n\n### Requirements\n\nA **Twain** account + API key with a welcome campaign; **Gmail** and **Slack**. Optional: a LinkedIn-lookup provider and **HeyReach** for LinkedIn steps.\n\n### How to customize\n\n- Route sequence steps to email vs LinkedIn with `email_steps` / `linkedin_steps`.\n- Swap the lookup provider, or enable a BigQuery / Firestore backfill trigger.\n\nBuilt with Twain \u2014 https://www.twain.ai"
      },
      "typeVersion": 1
    },
    {
      "id": "90ea72c4-85de-499b-8f14-4e344b99ec3b",
      "name": "Sticky: Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 1860,
        "height": 900,
        "content": "### 1 \u00b7 Intake & dedupe\n\nA new signup arrives via the webhook (or an optional BigQuery / Firestore backfill), is normalized to a common shape, and de-duplicated by `uid`."
      },
      "typeVersion": 1
    },
    {
      "id": "06655a66-9588-4e31-887b-970c2413b580",
      "name": "Sticky: Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        -528
      ],
      "parameters": {
        "color": 7,
        "width": 1800,
        "height": 1556,
        "content": "### 2 \u00b7 Resolve the contact\n\nWhen a signup has no LinkedIn URL, resolve one from the email so Twain has a real person to research. **Findymail** is the default provider; **Apollo, People Data Labs, and Proxycurl** are pre-built disabled alternatives \u2014 enable one and add its API-key credential."
      },
      "typeVersion": 1
    },
    {
      "id": "3f9bdf77-9ad8-4a8f-bc86-d0024e1400b4",
      "name": "Sticky: Section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3888,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 1120,
        "height": 900,
        "content": "### 3 \u00b7 Research & write\n\nTwain researches the person and generates the personalized welcome sequence; skipped signups and errors are reported to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "d410c035-abf6-424a-81e7-be233d401405",
      "name": "Sticky: Section 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5040,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 1980,
        "height": 900,
        "content": "### 4 \u00b7 Deliver & route\n\nWork email \u2192 Gmail draft; LinkedIn-only \u2192 Slack message to send manually; optional LinkedIn steps \u2192 HeyReach. Everything posted to Slack for approval \u2014 nothing auto-sends."
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "88db415c-dbd5-44bd-a587-779cf4c361b0",
  "nodeGroups": [],
  "connections": {
    "Signup Webhook": {
      "main": [
        [
          {
            "node": "Normalize Signup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Work Email?": {
      "main": [
        [
          {
            "node": "Gmail: Create Welcome Draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: LinkedIn welcome ready",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Channel Settings": {
      "main": [
        [
          {
            "node": "Dedupe New Signups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Signup": {
      "main": [
        [
          {
            "node": "Channel Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolved Contact": {
      "main": [
        [
          {
            "node": "Has usable contact?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dedupe New Signups": {
      "main": [
        [
          {
            "node": "Needs LinkedIn Lookup?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Welcome Draft": {
      "main": [
        [
          {
            "node": "Has Work Email?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has LinkedIn steps?": {
      "main": [
        [
          {
            "node": "HeyReach: Add to Campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has usable contact?": {
      "main": [
        [
          {
            "node": "Twain Generate Sequence",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Skipped (cannot personalize)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BigQuery: New signups": {
      "main": [
        [
          {
            "node": "Normalize Signup",
            "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
          }
        ]
      ]
    },
    "Twain Generate Sequence": {
      "main": [
        [
          {
            "node": "Build Welcome Draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Twain error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Firestore: New customers": {
      "main": [
        [
          {
            "node": "Normalize Signup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: Firestore poll": {
      "main": [
        [
          {
            "node": "Firestore: New customers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Draft not created": {
      "main": [
        [
          {
            "node": "Has LinkedIn steps?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: Email draft ready": {
      "main": [
        [
          {
            "node": "Has LinkedIn steps?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HeyReach: Add to Campaign": {
      "main": [
        [
          {
            "node": "Slack: LinkedIn steps queued (HeyReach)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Create Welcome Draft": {
      "main": [
        [
          {
            "node": "Slack: Email draft ready",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: Draft not created",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: BigQuery backfill": {
      "main": [
        [
          {
            "node": "BigQuery: New signups",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Lookup \u2014 Findymail": {
      "main": [
        [
          {
            "node": "Apply Resolved LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack: LinkedIn welcome ready": {
      "main": [
        [
          {
            "node": "Has LinkedIn steps?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}