This workflow corresponds to n8n.io template #16359 — we link there as the canonical source.
This workflow follows the Gmail → OpenAI 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 →
{
"id": "93WJ5MeVEIgDObFL",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Birthday Multi-Channel Notifier",
"tags": [
{
"id": "BdBRfTYYmsE2WeD8",
"name": "Published",
"createdAt": "2026-06-15T07:16:02.335Z",
"updatedAt": "2026-06-15T07:16:02.335Z"
}
],
"nodes": [
{
"id": "a790f342-ddfe-460a-900f-1a373d28360a",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1776,
3120
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.2
},
{
"id": "621d5711-a02a-475b-a56e-83b80f7e65e1",
"name": "Google Contacts",
"type": "n8n-nodes-base.googleContacts",
"notes": "Fetch contacts with names, birthdays, emails, phone numbers, and biographies. Biography can hold a Telegram chat ID if you choose that workaround.",
"position": [
2048,
3120
],
"parameters": {
"fields": [
"emailAddresses",
"birthdays",
"names",
"phoneNumbers",
"biographies"
],
"options": {},
"operation": "getAll",
"returnAll": true
},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "26459c96-a5c5-44d6-95ca-e0b2ee3a6cc0",
"name": "If WhatsApp",
"type": "n8n-nodes-base.if",
"notes": "Routes only contacts with a normalized phone number that fits WhatsApp validation.",
"position": [
3216,
3200
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.hasWhatsapp }}",
"rightValue": true
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "ad08b9db-da11-4dc6-a758-dfd1dcbbfff3",
"name": "WhatsApp Skipped",
"type": "n8n-nodes-base.set",
"notes": "Sets a readable status when a WhatsApp-safe phone number is not available.",
"position": [
3568,
3296
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "wa_status_false",
"name": "whatsappStatus",
"type": "string",
"value": "Skipped - no valid WhatsApp number"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "557cbebf-6243-4295-9ab0-57b6c8cbf455",
"name": "If Telegram",
"type": "n8n-nodes-base.if",
"notes": "Routes only contacts with a numeric Telegram chat ID.",
"position": [
3200,
3680
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.hasTelegram }}",
"rightValue": true
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "1c59c56e-0f66-40ba-87e9-b6a8e1a74c34",
"name": "Telegram Skipped",
"type": "n8n-nodes-base.set",
"notes": "Sets a readable status when Telegram delivery is not possible.",
"position": [
3552,
3776
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "tg_status_false",
"name": "telegramStatus",
"type": "string",
"value": "Skipped - no valid Telegram chat ID"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "51808cbd-05b2-4f5f-a2a3-76219f15bae6",
"name": "Sticky Input",
"type": "n8n-nodes-base.stickyNote",
"position": [
1648,
2864
],
"parameters": {
"color": 7,
"width": 320,
"height": 508,
"content": "## Input stage\n- **Schedule Trigger** runs daily at 8 AM.\n- **Google Contacts** fetches birthdays, names, emails, phone numbers, and biographies.\n- Biography can be used as a temporary place to store a Telegram chat ID."
},
"typeVersion": 1
},
{
"id": "11bb4274-1943-462c-b76b-2d2e84a5ef32",
"name": "Sticky Validation",
"type": "n8n-nodes-base.stickyNote",
"position": [
2016,
2928
],
"parameters": {
"color": 7,
"width": 476,
"height": 444,
"content": "## Validation stage\n**Normalize + Validate** cleans names, extracts birthday month/day, validates email, normalizes phone numbers for WhatsApp, and checks whether Telegram has a numeric chat ID. Only contacts with birthdays today continue."
},
"typeVersion": 1
},
{
"id": "7fc04b3e-b343-4c9d-80aa-96dce33b1963",
"name": "Sticky AI",
"type": "n8n-nodes-base.stickyNote",
"position": [
2560,
2928
],
"parameters": {
"color": 7,
"width": 456,
"height": 440,
"content": "## AI stage\n**AI Birthday Message** should return JSON with `subject`, `emailMessage`, and `shortMessage`. **Prepare Messages** adds safe fallback copy if the model returns incomplete data."
},
"typeVersion": 1
},
{
"id": "8376f7bb-f954-4e6f-b2f1-6c3181bf49ec",
"name": "Sticky Email",
"type": "n8n-nodes-base.stickyNote",
"position": [
3136,
2496
],
"parameters": {
"color": 7,
"width": 872,
"height": 484,
"content": "## Email branch\nSend through Gmail only when `hasEmail = true`. Sent and skipped states are both written back so the final summary stays readable."
},
"typeVersion": 1
},
{
"id": "1ae7ff0a-00cd-4a2d-81bc-fe9997bf2d76",
"name": "Sticky WhatsApp",
"type": "n8n-nodes-base.stickyNote",
"position": [
3136,
3008
],
"parameters": {
"color": 7,
"width": 868,
"height": 456,
"content": "## WhatsApp branch\nSend through WhatsApp Business Cloud only when the phone number passes validation. Replace `YOUR_WHATSAPP_PHONE_NUMBER_ID` and attach WhatsApp credentials before activation."
},
"typeVersion": 1
},
{
"id": "57d59661-83d4-427d-8b92-e6f502e1f89d",
"name": "Sticky Telegram",
"type": "n8n-nodes-base.stickyNote",
"position": [
3152,
3520
],
"parameters": {
"color": 7,
"width": 852,
"height": 504,
"content": "## Telegram branch\nTelegram delivery needs a real numeric `chatId` already known to your bot. If you only have a username or phone number, this branch is not reliable enough for production."
},
"typeVersion": 1
},
{
"id": "6ad2d7da-3de8-4579-a1dd-f4e0ff4a0245",
"name": "Sticky Summary",
"type": "n8n-nodes-base.stickyNote",
"position": [
4144,
2768
],
"parameters": {
"color": 7,
"width": 1116,
"height": 756,
"content": "## Final reporting\nThe merge nodes recombine all channel branches. **Build Summary** formats one Slack message per birthday contact, and **Slack Final Notification** posts the final operational result."
},
"typeVersion": 1
},
{
"id": "99379c2c-d59b-4c17-af4e-7590654647e6",
"name": "Sticky Checklist",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
2896
],
"parameters": {
"color": 4,
"width": 540,
"height": 332,
"content": "# Birthday reminders\nRuns every morning, pulls Google Contacts, filters contacts whose birthday is today, generates a personalized message, sends it through available channels, and posts a final Slack summary.\n\n## Approval checklist\n- Add Slack channel ID.\n- Add WhatsApp phone number ID.\n- Confirm OpenAI credentials and prompt output.\n- Confirm Google Contacts exposes `biographies` if you use it for Telegram IDs.\n- Test each branch manually before enabling the schedule."
},
"typeVersion": 1
},
{
"id": "006d5ab9-12ec-42ac-8c50-082d1271e618",
"name": "Normalize + Validate1",
"type": "n8n-nodes-base.code",
"notes": "Normalizes contact data, validates channel readiness, and keeps only contacts whose birthday matches today.",
"position": [
2320,
3120
],
"parameters": {
"jsCode": "const now = new Date();\nconst todayMonth = now.getMonth() + 1;\nconst todayDay = now.getDate();\n\nfunction getFirst(arr) {\n return Array.isArray(arr) && arr.length ? arr[0] : null;\n}\n\nfunction cleanPhone(value) {\n if (!value) return '';\n const cleaned = String(value).replace(/[^\\d+]/g, '');\n if (cleaned.startsWith('00')) return '+' + cleaned.slice(2);\n return cleaned;\n}\n\nfunction parseBirthday(birthdays) {\n if (!Array.isArray(birthdays) || birthdays.length === 0) return { month: null, day: null };\n const b = birthdays[0] || {};\n const d = b.date || {};\n return {\n month: Number(d.month || b.month || 0) || null,\n day: Number(d.day || b.day || 0) || null,\n };\n}\n\nreturn items.map((item) => {\n const j = item.json;\n const name = getFirst(j.names) || {};\n const emailObj = getFirst(j.emailAddresses) || {};\n const phoneObj = getFirst(j.phoneNumbers) || {};\n const birthday = parseBirthday(j.birthdays);\n\n const firstName = name.givenName || name.displayName || '';\n const lastName = name.familyName || '';\n const fullName = [firstName, lastName].filter(Boolean).join(' ').trim() || 'Friend';\n const email = (emailObj.value || '').trim();\n const whatsappNumber = cleanPhone(phoneObj.value || '');\n\n const telegramChatId = (j.biographies && j.biographies[0] && j.biographies[0].value)\n ? String(j.biographies[0].value).trim()\n : '';\n\n const hasBirthdayToday = birthday.month === todayMonth && birthday.day === todayDay;\n const hasEmail = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n const hasWhatsapp = /^\\+?[1-9]\\d{7,14}$/.test(whatsappNumber);\n const hasTelegram = /^-?\\d+$/.test(telegramChatId);\n\n return {\n json: {\n ...j,\n firstName,\n lastName,\n fullName,\n email,\n whatsappNumber,\n telegramChatId,\n birthdayMonth: birthday.month,\n birthdayDay: birthday.day,\n hasBirthdayToday,\n hasEmail,\n hasWhatsapp,\n hasTelegram,\n }\n };\n}).filter(item => item.json.hasBirthdayToday);"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "b2ca5f29-9d25-487f-80a3-c7a8055c8bf0",
"name": "AI Birthday Message1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"notes": "Generates one reusable birthday message object for email and chat channels.",
"position": [
2624,
3120
],
"parameters": {
"resource": "chat"
},
"notesInFlow": true,
"typeVersion": 1.8
},
{
"id": "bb6700e9-c125-4b78-93b0-3303d5062112",
"name": "Prepare Messages1",
"type": "n8n-nodes-base.code",
"notes": "Applies fallback message values if the AI response is empty or incomplete.",
"position": [
2864,
3120
],
"parameters": {
"jsCode": "const src = $input.first().json;\nconst subject = src.subject || `Happy Birthday ${src.firstName || src.fullName || ''}`.trim();\nconst emailMessage = src.emailMessage || `Wishing you a wonderful birthday, ${src.firstName || src.fullName || 'Friend'}! Have a fantastic day ahead.`;\nconst shortMessage = src.shortMessage || `Happy Birthday ${src.firstName || ''}! Wishing you a fantastic day!`.trim();\n\nreturn [{\n json: {\n ...src,\n subject,\n emailMessage,\n shortMessage\n }\n}];"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "80d7fc84-ce76-47c8-b8dd-e479ae96dd60",
"name": "If Email1",
"type": "n8n-nodes-base.if",
"notes": "Routes only contacts with a validated email address to Gmail.",
"position": [
3184,
2720
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.hasEmail }}",
"rightValue": true
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "b97e6227-2717-475f-9837-f68d89bfe1e6",
"name": "Gmail1",
"type": "n8n-nodes-base.gmail",
"notes": "Sends the personalized birthday email only when email validation passed.",
"position": [
3536,
2624
],
"parameters": {
"sendTo": "={{ $json.email }}",
"message": "={{ $json.emailMessage }}",
"options": {
"appendAttribution": false
},
"subject": "={{ $json.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 2.1
},
{
"id": "9c3545b3-d728-47c9-95fa-78fe8e1163dc",
"name": "Email Skipped1",
"type": "n8n-nodes-base.set",
"notes": "Sets a readable status when email is unavailable or invalid.",
"position": [
3536,
2816
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "email_status_false",
"name": "emailStatus",
"type": "string",
"value": "Skipped - no valid email"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "bbc188bb-c70d-46be-aa4f-acd32dc3fa8f",
"name": "Email Sent1",
"type": "n8n-nodes-base.set",
"notes": "Marks the email delivery path as sent for the final summary.",
"position": [
3776,
2624
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "email_status_true",
"name": "emailStatus",
"type": "string",
"value": "Sent"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "d830f644-74d8-4b3b-8ba4-c625f78d63eb",
"name": "WhatsApp1",
"type": "n8n-nodes-base.whatsApp",
"notes": "Sends a short birthday message through WhatsApp Business Cloud. Replace the phoneNumberId and attach valid credentials.",
"position": [
3568,
3136
],
"parameters": {
"textBody": "={{ $json.shortMessage }}",
"operation": "send",
"phoneNumberId": "YOUR_WHATSAPP_PHONE_NUMBER_ID",
"additionalFields": {
"previewUrl": false
},
"recipientPhoneNumber": "={{ $json.whatsappNumber }}"
},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "e9dc6509-d4cb-4b26-b973-9514c337fde4",
"name": "WhatsApp Sent1",
"type": "n8n-nodes-base.set",
"notes": "Marks the WhatsApp delivery path as sent for the final summary.",
"position": [
3808,
3136
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "wa_status_true",
"name": "whatsappStatus",
"type": "string",
"value": "Sent"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "c2cc1606-caa2-4ad9-8db8-d320b5fd1479",
"name": "Telegram1",
"type": "n8n-nodes-base.telegram",
"notes": "Sends a Telegram birthday message. This will fail without a valid bot-accessible chat ID.",
"position": [
3552,
3616
],
"parameters": {
"text": "={{ $json.shortMessage }}",
"chatId": "={{ $json.telegramChatId }}",
"additionalFields": {
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 1.2
},
{
"id": "f7ddaf08-c5b7-4c1f-bb70-fb06f5e59105",
"name": "Telegram Sent1",
"type": "n8n-nodes-base.set",
"notes": "Marks the Telegram delivery path as sent for the final summary.",
"position": [
3792,
3616
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "tg_status_true",
"name": "telegramStatus",
"type": "string",
"value": "Sent"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "9e70ff45-0748-48bd-9721-b690072ddba6",
"name": "Merge Email1",
"type": "n8n-nodes-base.merge",
"notes": "Merges the sent and skipped email branches back into one item stream.",
"position": [
4160,
2976
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"notesInFlow": true,
"typeVersion": 3.2
},
{
"id": "0bd03599-3ec6-4ad3-abc3-e41e1fd03f01",
"name": "Merge WhatsApp1",
"type": "n8n-nodes-base.merge",
"notes": "Combines the email result with the WhatsApp sent or skipped branch.",
"position": [
4400,
3152
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"notesInFlow": true,
"typeVersion": 3.2
},
{
"id": "9ed1abdb-e8b1-4ac0-988d-cfc46ed765cf",
"name": "Merge Telegram1",
"type": "n8n-nodes-base.merge",
"notes": "Combines prior channel results with the Telegram sent or skipped branch.",
"position": [
4640,
3312
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"notesInFlow": true,
"typeVersion": 3.2
},
{
"id": "811b9cd7-4ea3-42f6-b781-4f683cacccbd",
"name": "Build Summary1",
"type": "n8n-nodes-base.code",
"notes": "Builds the final Slack-friendly summary showing sent or skipped status per channel.",
"position": [
4880,
3312
],
"parameters": {
"jsCode": "return items.map(item => {\n const j = item.json;\n const lines = [\n '\ud83c\udf82 Birthday automation completed',\n '',\n `Contact: ${j.fullName}`,\n `Email: ${j.emailStatus || 'Unknown'}`,\n `WhatsApp: ${j.whatsappStatus || 'Unknown'}`,\n `Telegram: ${j.telegramStatus || 'Unknown'}`\n ];\n\n return {\n json: {\n ...j,\n slackText: lines.join('\\n')\n }\n };\n});"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "4c391b1f-36d0-4048-8323-06e677c350c1",
"name": "Slack Final Notification1",
"type": "n8n-nodes-base.slack",
"notes": "Posts the final ops summary to Slack after all channel branches finish.",
"position": [
5120,
3312
],
"parameters": {
"text": "={{ $json.slackText }}",
"select": "channel",
"channelId": "YOUR_SLACK_CHANNEL_ID",
"otherOptions": {},
"authentication": "oAuth2"
},
"notesInFlow": true,
"typeVersion": 2.3
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "e895e0dd-d027-4dab-b1ff-b6079c6cd13e",
"nodeGroups": [],
"connections": {
"Gmail1": {
"main": [
[
{
"node": "Email Sent1",
"type": "main",
"index": 0
}
]
]
},
"If Email1": {
"main": [
[
{
"node": "Gmail1",
"type": "main",
"index": 0
}
],
[
{
"node": "Email Skipped1",
"type": "main",
"index": 0
}
]
]
},
"Telegram1": {
"main": [
[
{
"node": "Telegram Sent1",
"type": "main",
"index": 0
}
]
]
},
"WhatsApp1": {
"main": [
[
{
"node": "WhatsApp Sent1",
"type": "main",
"index": 0
}
]
]
},
"Email Sent1": {
"main": [
[
{
"node": "Merge Email1",
"type": "main",
"index": 0
}
]
]
},
"If Telegram": {
"main": [
[
{
"node": "Telegram1",
"type": "main",
"index": 0
}
],
[
{
"node": "Telegram Skipped",
"type": "main",
"index": 0
}
]
]
},
"If WhatsApp": {
"main": [
[
{
"node": "WhatsApp1",
"type": "main",
"index": 0
}
],
[
{
"node": "WhatsApp Skipped",
"type": "main",
"index": 0
}
]
]
},
"Merge Email1": {
"main": [
[
{
"node": "Merge WhatsApp1",
"type": "main",
"index": 0
}
]
]
},
"Build Summary1": {
"main": [
[
{
"node": "Slack Final Notification1",
"type": "main",
"index": 0
}
]
]
},
"Email Skipped1": {
"main": [
[
{
"node": "Merge Email1",
"type": "main",
"index": 1
}
]
]
},
"Telegram Sent1": {
"main": [
[
{
"node": "Merge Telegram1",
"type": "main",
"index": 1
}
]
]
},
"WhatsApp Sent1": {
"main": [
[
{
"node": "Merge WhatsApp1",
"type": "main",
"index": 1
}
]
]
},
"Google Contacts": {
"main": [
[
{
"node": "Normalize + Validate1",
"type": "main",
"index": 0
}
]
]
},
"Merge Telegram1": {
"main": [
[
{
"node": "Build Summary1",
"type": "main",
"index": 0
}
]
]
},
"Merge WhatsApp1": {
"main": [
[
{
"node": "Merge Telegram1",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Google Contacts",
"type": "main",
"index": 0
}
]
]
},
"Telegram Skipped": {
"main": [
[
{
"node": "Merge Telegram1",
"type": "main",
"index": 1
}
]
]
},
"WhatsApp Skipped": {
"main": [
[
{
"node": "Merge WhatsApp1",
"type": "main",
"index": 1
}
]
]
},
"Prepare Messages1": {
"main": [
[
{
"node": "If Email1",
"type": "main",
"index": 0
},
{
"node": "If WhatsApp",
"type": "main",
"index": 0
},
{
"node": "If Telegram",
"type": "main",
"index": 0
}
]
]
},
"AI Birthday Message1": {
"main": [
[
{
"node": "Prepare Messages1",
"type": "main",
"index": 0
}
]
]
},
"Normalize + Validate1": {
"main": [
[
{
"node": "AI Birthday Message1",
"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.
gmailOAuth2telegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily, pulls birthdays from Google Contacts, generates personalized birthday copy with OpenAI, sends greetings via Gmail, WhatsApp Business Cloud, and Telegram when available, and posts a per-contact delivery summary to Slack. Runs every day at 8:00 AM on a…
Source: https://n8n.io/workflows/16359/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow monitors new hires in Google Sheets, downloads onboarding documents from Google Drive, uses OpenAI (gpt-4o-mini) to verify each file and determine overall compliance, then notifies HR vi
This workflow automates rent reminders and tenant support by combining Google Sheets as the system of record with Telegram messaging, OpenAI (gpt-4o-mini) intent classification and receipt writing, an
This workflow runs daily to forecast SKU demand from Google Sheets data, drafts purchase-order justifications with OpenAI, routes critical and warning items for Telegram review and Slack alerts, logs
This workflow runs daily at 8am to detect meaningful changes in competitor pricing, Google search ranking, revenue anomalies, and customer churn using Google Sheets, SerpApi, and OpenAI, then sends a
This workflow runs weekly to pull HR datasets from Google Sheets, calculate rule-based attrition risk signals, and enrich them with an OpenAI (GPT-4o-mini) risk assessment. It logs results back to Goo