This workflow follows the Execute Workflow Trigger → Gmail 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": "summarize_emails",
"nodes": [
{
"parameters": {},
"id": "execute-workflow-trigger",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
240,
300
]
},
{
"parameters": {
"jsCode": "// Prepare parameters for email fetching\nconst input = $input.all()[0].json || {};\n\n// Default to today's emails if no specific timeframe provided\nconst hoursBack = input.hoursBack || 24;\nconst maxEmails = input.maxEmails || 20;\nconst onlyUnread = input.onlyUnread !== false; // default true\n\n// Calculate date filter (Gmail uses YYYY/MM/DD format)\nconst cutoffDate = new Date();\ncutoffDate.setHours(cutoffDate.getHours() - hoursBack);\nconst dateFilter = cutoffDate.toISOString().split('T')[0].replace(/-/g, '/');\n\n// Build Gmail search query\nlet query = `after:${dateFilter}`;\nif (onlyUnread) {\n query += ' is:unread';\n}\n// Exclude automated emails\nquery += ' -from:noreply -from:no-reply -from:donotreply';\n\nreturn [{\n json: {\n query: query,\n maxEmails: maxEmails,\n hoursBack: hoursBack,\n dateFilter: dateFilter,\n onlyUnread: onlyUnread\n }\n}];"
},
"id": "prepare-email-query",
"name": "Prepare Email Query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"resource": "message",
"operation": "getAll",
"returnAll": false,
"limit": "={{ $json.maxEmails }}",
"filters": {
"q": "={{ $json.query }}"
},
"options": {
"format": "full",
"includeSpamTrash": false
}
},
"id": "fetch-emails",
"name": "Fetch Recent Emails",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
680,
300
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Process and clean email data for AI summarization\nconst items = $input.all();\n\nif (items.length === 0 || !items[0].json) {\n return [{\n json: {\n emailCount: 0,\n summary: \"No recent emails found to summarize.\",\n ok: true,\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString()\n }\n }];\n}\n\nconst emails = items.map(item => item.json);\n\n// Helper function to extract plain text from email body\nfunction extractTextFromEmail(email) {\n let text = '';\n \n if (email.payload) {\n // Handle multipart emails\n if (email.payload.parts) {\n for (const part of email.payload.parts) {\n if (part.mimeType === 'text/plain' && part.body.data) {\n text += Buffer.from(part.body.data, 'base64').toString('utf-8');\n }\n }\n }\n // Handle single part emails\n else if (email.payload.mimeType === 'text/plain' && email.payload.body.data) {\n text = Buffer.from(email.payload.body.data, 'base64').toString('utf-8');\n }\n }\n \n // Clean up the text\n return text\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n .substring(0, 1000); // Limit to 1000 chars per email\n}\n\n// Helper function to get header value\nfunction getHeader(email, name) {\n if (!email.payload || !email.payload.headers) return '';\n const header = email.payload.headers.find(h => h.name.toLowerCase() === name.toLowerCase());\n return header ? header.value : '';\n}\n\n// Process emails for summarization\nconst processedEmails = emails.map((email, index) => {\n return {\n from: getHeader(email, 'From'),\n subject: getHeader(email, 'Subject'),\n date: getHeader(email, 'Date'),\n snippet: email.snippet || '',\n bodyText: extractTextFromEmail(email)\n };\n}).filter(email => \n email.from && email.subject && \n !email.from.includes('noreply') && \n !email.from.includes('no-reply')\n);\n\nif (processedEmails.length === 0) {\n return [{\n json: {\n emailCount: 0,\n summary: \"No relevant emails found to summarize (filtered out automated emails).\",\n ok: true,\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString(),\n needsSummary: false // Explicitly set to false\n }\n }];\n}\n\n// Prepare data for Gemini API call\nconst emailsForAI = processedEmails.slice(0, 10); // Limit to 10 emails max\n\nreturn [{\n json: {\n emailCount: emailsForAI.length,\n emails: emailsForAI,\n needsSummary: true\n }\n}];"
},
"id": "process-emails",
"name": "Process Email Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "needs-summary-check",
"leftValue": "={{ $json.needsSummary }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "needs-summary-check",
"name": "Needs AI Summary?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1120,
300
]
},
{
"parameters": {
"jsCode": "// Prepare Gemini API request for email summarization\nconst data = $input.all()[0].json;\n\n// Add a safeguard in case this node is reached without emails\nif (!data.emails || data.emails.length === 0) {\n return [{\n json: {\n ok: false,\n error: \"Attempted to summarize with no emails.\",\n code: \"LOGIC_ERROR\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString()\n }\n }];\n}\n\nconst emails = data.emails;\n\n// Build context for Gemini\nconst emailContext = emails.map((email, i) => \n `EMAIL ${i + 1}:\n` +\n `From: ${email.from}\n` +\n `Subject: ${email.subject}\n` +\n `Date: ${email.date}\n` +\n `Content: ${email.bodyText || email.snippet}\n` +\n `---`\n).join('\\n\\n');\n\nconst prompt = `You are an AI assistant helping summarize recent emails. Please analyze these ${emails.length} emails and create a concise, organized summary.\n\n${emailContext}\n\nPlease provide a summary in this format:\n\n# Email Summary (${emails.length} emails)\n\n## \ud83d\udd25 Urgent/Important\n[List any urgent emails requiring immediate attention]\n\n## \ud83d\udcc5 Meetings & Events\n[List any meeting invitations, calendar events, or scheduling]\n\n## \ud83d\udcbc Work Updates\n[List work-related updates, project communications, etc.]\n\n## \ud83d\udce7 Other Communications\n[List other notable emails]\n\n## \ud83d\udcca Summary Stats\n- Total emails: ${emails.length}\n- Time period: Last 24 hours\n- Unread count: [count if available]\n\nKeep it concise but informative. Focus on actionable items and important communications.`;\n\nconst geminiPayload = {\n contents: [{\n parts: [{\n text: prompt\n }]\n }],\n generationConfig: {\n temperature: 0.3,\n topK: 40,\n topP: 0.95,\n maxOutputTokens: 1000\n }\n};\n\nreturn [{\n json: {\n geminiPayload: geminiPayload,\n emailCount: emails.length,\n originalData: data\n }\n}];"
},
"id": "prepare-gemini-request",
"name": "Prepare Gemini Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
480
]
},
{
"parameters": {
"jsCode": "// Call Gemini API using built-in fetch\nconst inputItems = $input.all();\n\n// Debug: Check if we have input data\nif (!inputItems || inputItems.length === 0) {\n return [{\n json: {\n ok: false,\n error: \"No input data received\",\n code: \"NO_INPUT_DATA\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString(),\n debug: { inputLength: inputItems ? inputItems.length : 'undefined' }\n }\n }];\n}\n\nconst data = inputItems[0].json;\n\n// Safeguard in case this node is reached without proper data\nif (!data || !data.geminiPayload) {\n return [{\n json: {\n ok: false,\n error: \"No Gemini payload found\",\n code: \"LOGIC_ERROR\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString(),\n debug: { \n hasData: !!data,\n hasGeminiPayload: !!(data && data.geminiPayload),\n dataKeys: data ? Object.keys(data) : 'no data'\n }\n }\n }];\n}\n\nconst geminiPayload = data.geminiPayload;\nconst apiKey = process.env.GEMINI_API_KEY || $env.GEMINI_API_KEY;\nconst url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`;\n\nif (!apiKey) {\n return [{\n json: {\n ok: false,\n error: \"GEMINI_API_KEY not configured\",\n code: \"CONFIGURATION_ERROR\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString()\n }\n }];\n}\n\ntry {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(geminiPayload)\n });\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const responseData = await response.json();\n\n return [{\n json: responseData\n }];\n} catch (error) {\n console.error('Gemini API error:', error);\n return [{\n json: {\n ok: false,\n error: \"Failed to generate AI summary\",\n code: \"GEMINI_API_ERROR\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString(),\n fallbackSummary: `Found ${data.emailCount || 0} recent emails. AI summarization temporarily unavailable.`,\n errorDetails: error.message\n }\n }];\n}"
},
"id": "call-gemini-api",
"name": "Call Gemini API",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
480
]
},
{
"parameters": {
"jsCode": "// Process Gemini response and format final output\nconst inputItems = $input.all();\n\n// Debug: Check if we have input data\nif (!inputItems || inputItems.length === 0) {\n return [{\n json: {\n ok: false,\n error: \"No input data received in format response\",\n code: \"NO_INPUT_DATA\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString()\n }\n }];\n}\n\nconst geminiResponse = inputItems[0].json;\n\n// Check if this is an error response from the Gemini API call\nif (!geminiResponse) {\n return [{\n json: {\n ok: false,\n error: \"No response data from Gemini API\",\n code: \"EMPTY_RESPONSE\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString()\n }\n }];\n}\n\n// If it's already an error response, pass it through\nif (geminiResponse.ok === false) {\n return [{ json: geminiResponse }];\n}\n\ntry {\n // Extract the summary from Gemini response\n let summary = \"Unable to generate summary.\";\n \n if (geminiResponse.candidates && \n geminiResponse.candidates[0] && \n geminiResponse.candidates[0].content && \n geminiResponse.candidates[0].content.parts && \n geminiResponse.candidates[0].content.parts[0]) {\n \n summary = geminiResponse.candidates[0].content.parts[0].text;\n }\n \n // Clean up the summary\n summary = summary\n .replace(/\\*\\*/g, '**') // Ensure markdown formatting\n .replace(/\\n{3,}/g, '\\n\\n') // Clean excessive newlines\n .trim();\n \n return [{\n json: {\n ok: true,\n summary: summary,\n emailCount: geminiResponse.emailCount || 'unknown',\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString(),\n metadata: {\n generatedBy: \"gemini-2.0-flash-exp\",\n processingTime: new Date().toISOString()\n }\n }\n }];\n \n} catch (error) {\n // Handle Gemini API errors gracefully\n console.error('Gemini API error:', error);\n \n return [{\n json: {\n ok: false,\n error: \"Failed to process AI summary\",\n code: \"PROCESSING_ERROR\",\n brick: \"summarize_emails\",\n timestamp: new Date().toISOString(),\n fallbackSummary: \"AI summarization temporarily unavailable.\",\n errorDetails: error.message\n }\n }];\n}"
},
"id": "format-final-response",
"name": "Format Final Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
480
]
}
],
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Prepare Email Query",
"type": "main",
"index": 0
}
]
]
},
"Prepare Email Query": {
"main": [
[
{
"node": "Fetch Recent Emails",
"type": "main",
"index": 0
}
]
]
},
"Fetch Recent Emails": {
"main": [
[
{
"node": "Process Email Data",
"type": "main",
"index": 0
}
]
]
},
"Process Email Data": {
"main": [
[
{
"node": "Needs AI Summary?",
"type": "main",
"index": 0
}
]
]
},
"Needs AI Summary?": {
"main": [
[],
[
{
"node": "Prepare Gemini Request",
"type": "main",
"index": 0
}
]
]
},
"Prepare Gemini Request": {
"main": [
[
{
"node": "Call Gemini API",
"type": "main",
"index": 0
}
]
]
},
"Call Gemini API": {
"main": [
[
{
"node": "Format Final Response",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": {},
"tags": [
"pulse",
"brick",
"email",
"ai"
],
"triggerCount": 0,
"updatedAt": "2025-08-04T00:00:00.000Z",
"versionId": "2"
}
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.
gmailOAuth2
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
summarize_emails. Uses executeWorkflowTrigger, gmail. Event-driven trigger; 8 nodes.
Source: https://github.com/SammyTourani/Pulse/blob/09d51f209c603477a489582b13f5c08d9a0af370/flows/bricks/summarize_emails_clean.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.
Echo Brand Voice Analysis (Processor) - TASK-074 Dec 10 Fix. Uses formTrigger, httpRequest, executeWorkflowTrigger, moveBinaryData. Event-driven trigger; 40 nodes.
Code Filter. Uses googleSheets, gmail, stickyNote, executeWorkflowTrigger. Event-driven trigger; 32 nodes.
This n8n workflow enables teams to automate and standardize multi-step onboarding or messaging workflows using Google Sheets, Forms, Gmail, and dynamic logic powered by Code and Switch nodes. It ensur