This workflow follows the Gmail → Google Sheets 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 →
{
"name": "LAB4",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-400,
448
],
"id": "1a00a301-39cc-463a-be68-f3ea1991fdd2",
"name": "When clicking \u2018Execute workflow\u2019"
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1FcJV3CPgLRKstIbLQeIV1t0cVOmrkXL3Pugr2SHTOWM",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "Feuille 1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1FcJV3CPgLRKstIbLQeIV1t0cVOmrkXL3Pugr2SHTOWM/edit#gid=0"
},
"options": {
"dataLocationOnSheet": {
"values": {
"rangeDefinition": "specifyRangeA1",
"range": "A1:A"
}
},
"outputFormatting": {
"values": {
"general": "UNFORMATTED_VALUE",
"date": "FORMATTED_STRING"
}
}
}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
64,
624
],
"id": "ebec14e6-ea00-42b7-83df-2015c05207e5",
"name": "Get row(s) in sheet",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"sendTo": "={{$json[\"email\"]}}",
"subject": "=Weekly Newsletter \u2013 {{ $now.startOf('week').toISODate() }}",
"message": "={{ $json.html }}",
"options": {}
},
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
880,
544
],
"id": "649a34b8-4a08-4d0e-b063-21f27acfe0f1",
"name": "Send a message",
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
720,
528
],
"id": "a6ed6964-e906-4749-a478-ad9a5f517cc1",
"name": "Merge"
},
{
"parameters": {
"jsCode": "/**\n * N8N Email Template Generator for Spotify Singles Newsletter\n * \n * Fixed version that handles multiple items with singles arrays\n * Place this in a Function node BEFORE the Send Email node\n */\n\n// Get ALL items from input\nconst allItems = $input.all();\n\n// Flatten all singles from all items into one array\nlet allSingles = [];\nallItems.forEach(item => {\n if (item.json.singles && Array.isArray(item.json.singles)) {\n allSingles = allSingles.concat(item.json.singles);\n }\n});\n\n// Remove duplicates (by albumId)\nconst uniqueSingles = [];\nconst seenIds = new Set();\n\nallSingles.forEach(single => {\n if (!seenIds.has(single.albumId)) {\n uniqueSingles.push(single);\n seenIds.add(single.albumId);\n }\n});\n\nconst singles = uniqueSingles;\nconst currentYear = new Date().getFullYear();\n\n// Generate HTML for each single\nconst singlesHtml = singles.map(single => `\n <div class=\"single-card\">\n <img src=\"${single.cover || 'https://via.placeholder.com/300'}\" alt=\"${single.title}\" class=\"single-cover\" />\n <div class=\"single-info\">\n <div class=\"single-title\">${single.title}</div>\n <div class=\"single-artists\">${single.artistsString}</div>\n <div class=\"single-date\">\ud83d\udcc5 ${single.releaseDate}</div>\n <a href=\"${single.link}\" target=\"_blank\" class=\"single-link\">Listen on Spotify \u2192</a>\n </div>\n </div>\n`).join('');\n\n// Generate complete HTML email\nconst htmlEmail = `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Latest Spotify Singles</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n background-color: #f5f5f5;\n color: #333;\n }\n \n .container {\n max-width: 800px;\n margin: 0 auto;\n background-color: #ffffff;\n }\n \n .header {\n background: linear-gradient(135deg, #1DB954 0%, #191414 100%);\n padding: 40px 20px;\n text-align: center;\n color: white;\n }\n \n .header h1 {\n font-size: 32px;\n margin-bottom: 8px;\n font-weight: 700;\n }\n \n .header p {\n font-size: 14px;\n opacity: 0.9;\n }\n \n .content {\n padding: 40px 20px;\n }\n \n .intro {\n font-size: 16px;\n color: #555;\n margin-bottom: 30px;\n line-height: 1.6;\n }\n \n .singles-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\n gap: 20px;\n margin-bottom: 40px;\n }\n \n .single-card {\n background: #f9f9f9;\n border: 1px solid #e0e0e0;\n border-radius: 8px;\n overflow: hidden;\n }\n \n .single-cover {\n width: 100%;\n max-height: 300px;\n object-fit: cover;\n background: #ddd;\n display: block;\n }\n \n .single-info {\n padding: 16px;\n }\n \n .single-title {\n font-size: 16px;\n font-weight: 600;\n color: #1DB954;\n margin-bottom: 8px;\n line-height: 1.4;\n }\n \n .single-artists {\n font-size: 13px;\n color: #666;\n margin-bottom: 12px;\n }\n \n .single-date {\n font-size: 12px;\n color: #999;\n margin-bottom: 12px;\n }\n \n .single-link {\n display: inline-block;\n background-color: #1DB954;\n color: white;\n padding: 8px 16px;\n border-radius: 20px;\n text-decoration: none;\n font-size: 12px;\n font-weight: 600;\n }\n \n .single-link:hover {\n background-color: #1ed760;\n }\n \n .stats {\n background: #f0f0f0;\n padding: 20px;\n border-radius: 8px;\n margin-bottom: 30px;\n text-align: center;\n }\n \n .stats-number {\n font-size: 32px;\n font-weight: 700;\n color: #1DB954;\n }\n \n .stats-label {\n font-size: 14px;\n color: #666;\n margin-top: 4px;\n }\n \n .footer {\n background-color: #191414;\n color: #b3b3b3;\n padding: 30px 20px;\n text-align: center;\n font-size: 12px;\n border-top: 1px solid #282828;\n }\n \n .footer a {\n color: #1DB954;\n text-decoration: none;\n }\n \n @media (max-width: 600px) {\n .singles-grid {\n grid-template-columns: 1fr !important;\n }\n \n .header h1 {\n font-size: 24px;\n }\n \n .content {\n padding: 20px;\n }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header\">\n <h1>\ud83c\udfb5 Latest Spotify Singles</h1>\n <p>Your weekly dose of new music</p>\n </div>\n \n <!-- Content -->\n <div class=\"content\">\n <p class=\"intro\">\n Hey there! \ud83c\udfa7 Check out the latest singles from your favorite artists. Click on any single to listen now on Spotify!\n </p>\n \n <!-- Stats -->\n <div class=\"stats\">\n <div class=\"stats-number\">${singles.length}</div>\n <div class=\"stats-label\">New Singles</div>\n </div>\n \n <!-- Singles Grid -->\n <div class=\"singles-grid\">\n ${singlesHtml}\n </div>\n </div>\n \n <!-- Footer -->\n <div class=\"footer\">\n <p>\u00a9 ${currentYear} Spotify Singles Newsletter</p>\n <p style=\"margin-top: 12px;\">\n You're receiving this because you subscribed to our newsletter.\n </p>\n </div>\n </div>\n</body>\n</html>\n`;\n\n// Return the email data for Send Email node\nreturn {\n json: {\n html: htmlEmail,\n subject: `\ud83c\udfb5 ${singles.length} New Spotify Singles - ${new Date().toLocaleDateString()}`,\n singlesCount: singles.length,\n singles: singles,\n debug: {\n totalItems: allItems.length,\n totalSingles: allSingles.length,\n uniqueSingles: singles.length\n }\n }\n};"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
944,
192
],
"id": "5b2a6992-fae8-4011-87dc-a5ec4005071e",
"name": "Code in JavaScript1"
},
{
"parameters": {
"url": "=https://api.spotify.com/v1/artists/{{ $json[\"artists.items[0].id\"] }}/albums?include_groups=single&limit=1 ",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "spotifyOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
432,
192
],
"id": "16094b8a-a076-4b49-8666-a121b4f4bae9",
"name": "HTTP Request",
"credentials": {
"spotifyOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"url": "=https://api.spotify.com/v1/search?q={{encodeURIComponent($json[\"artist_name\"])}}&type=artist&limit=1",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "spotifyOAuth2Api",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
-32,
192
],
"id": "842e4a2f-0137-4d8e-8fd8-f5d64c5b81c8",
"name": "HTTP Request1",
"credentials": {
"spotifyOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1BmpSC75hXh4_p08nCtzdG0UewwTuDHysNqvKdUx9Ybg",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "Feuille 1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1BmpSC75hXh4_p08nCtzdG0UewwTuDHysNqvKdUx9Ybg/edit#gid=0"
},
"options": {}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
-192,
288
],
"id": "30bd96fc-9cbb-414d-8227-d259aa948237",
"name": "Get row(s) in sheet1",
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"fieldToSplitOut": "artists.items[0].id",
"options": {}
},
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
208,
192
],
"id": "f260abf7-8b3e-42a8-8cbb-3baddf1f0b76",
"name": "Split Out"
},
{
"parameters": {
"jsCode": "return $input.all().map(item => {\n const albumData = item.json;\n const singles = [];\n\n // Process each artist's albums response\n if (albumData.items && Array.isArray(albumData.items)) {\n albumData.items.forEach(album => {\n // Check if this single already exists in our array\n const existingSingle = singles.find(s => s.albumId === album.id);\n\n if (existingSingle) {\n // Merge artists if single already exists\n const newArtists = album.artists\n .map(artist => artist.name)\n .filter(name => !existingSingle.artists.includes(name));\n \n existingSingle.artists = [...existingSingle.artists, ...newArtists];\n existingSingle.artistsString = existingSingle.artists.join(', ');\n } else {\n // Create new single entry\n singles.push({\n albumId: album.id,\n title: album.name,\n artists: album.artists.map(artist => artist.name),\n artistsString: album.artists.map(artist => artist.name).join(', '),\n cover: album.images && album.images.length > 0 ? album.images[0].url : null,\n coverThumb: album.images && album.images.length > 1 ? album.images[1].url : null,\n link: album.external_urls?.spotify || null,\n spotifyUri: album.uri || null,\n releaseDate: album.release_date || null,\n releaseDatePrecision: album.release_date_precision || null,\n totalTracks: album.total_tracks || 1,\n albumType: album.album_type || 'single'\n });\n }\n });\n }\n\n return {\n ...item,\n json: {\n singles: singles,\n count: singles.length,\n processedAt: new Date().toISOString()\n }\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
672,
48
],
"id": "ae5c898a-198e-4b07-b4d8-02d89fd7c9a5",
"name": "Code in JavaScript2"
}
],
"connections": {
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Get row(s) in sheet",
"type": "main",
"index": 0
},
{
"node": "Get row(s) in sheet1",
"type": "main",
"index": 0
}
]
]
},
"Get row(s) in sheet": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Send a message": {
"main": [
[]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Code in JavaScript2",
"type": "main",
"index": 0
}
]
]
},
"Get row(s) in sheet1": {
"main": [
[
{
"node": "HTTP Request1",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request1": {
"main": [
[
{
"node": "Split Out",
"type": "main",
"index": 0
}
]
]
},
"Split Out": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript2": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "cf6bc91f-5fd4-4e4d-9a1f-ab2d58086496",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "w9vGCP5ig765aIS4",
"tags": [
{
"name": "v3.0",
"id": "ED459mqoDxyM5BWn",
"updatedAt": "2025-12-05T20:39:21.521Z",
"createdAt": "2025-12-05T20:39:21.521Z"
}
]
}
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.
gmailOAuth2googleSheetsOAuth2ApispotifyOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
LAB4. Uses googleSheets, gmail, httpRequest. Event-driven trigger; 10 nodes.
Source: https://github.com/FRWD789/n8nNewsLettre/blob/037622d9671e103d83d1f537c6296cf87099d1a2/workflows/LAB4.json — 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.
Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.
Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.
AICARE Email Blast System. Uses googleDrive, httpRequest, googleSheets, gmail. Event-driven trigger; 39 nodes.
Tiktok to video copy (paylaşılacak). Uses httpRequest, googleDrive, googleSheets, gmail. Event-driven trigger; 39 nodes.
Automatically processes new orders added to Google Sheets. Small orders are approved instantly; large orders trigger an HTML email with one-click Approve / Reject links — each handled by an independen