AutomationFlowsAI & RAG › Summarize Meeting Transcripts with Gpt-4 & Sentiment Analysis for Gmail

Summarize Meeting Transcripts with Gpt-4 & Sentiment Analysis for Gmail

ByDhinesh Ravikumar @dk10rk on n8n.io

Project managers, AI builders, and teams who want structured, automated meeting summaries with zero manual work.

Event trigger★★★★☆ complexityAI-powered18 nodesGoogle Drive TriggerGoogle DriveAgentOpenAI ChatGmail
AI & RAG Trigger: Event Nodes: 18 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Agent → 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 →

Download .json
{
  "id": "7O3XDyjnKZuQ1iOB",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Email_Summarizer",
  "tags": [],
  "nodes": [
    {
      "id": "72bc448c-16e2-43f5-a3c3-5332183a91a7",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        416,
        -32
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyHour"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": "1IsQ3KyyfiexcYPlOiAkzYaH4MHI4VBPZ",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1IsQ3KyyfiexcYPlOiAkzYaH4MHI4VBPZ",
          "cachedResultName": "GraphRag"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6d7063b9-8ff3-4a3a-83a0-b97e4ea64311",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        1088,
        -128
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1
    },
    {
      "id": "6794155c-c90a-4876-828f-dfdda5c9ab22",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        640,
        -32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "14105474-d15e-477a-8916-cef9313887f7",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.fileExtension }}",
              "rightValue": "pdf"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "59d342dc-011a-420f-ba22-330fda11c4fd",
      "name": "Extract from File1",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        1088,
        64
      ],
      "parameters": {
        "options": {},
        "operation": "text"
      },
      "typeVersion": 1
    },
    {
      "id": "0b9f534e-b534-4f81-b760-2b95bd9be3f4",
      "name": "Download file",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        864,
        -128
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.id }}"
        },
        "options": {},
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "1686d72f-6448-496b-b8fb-a98f7a559dc3",
      "name": "Download file1",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        864,
        64
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Google Drive Trigger').item.json.parents[0] }}"
        },
        "options": {},
        "operation": "download"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "6f459193-17b1-4313-a251-b56febdaa721",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1312,
        -32
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "84894f45-b8a8-4837-98f4-5eefe64a720f",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1536,
        -32
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "options": {
          "systemMessage": "You are a helpful You are a rigorous meeting assistant. Convert the provided meeting text into a project plan.\nReturn ONLY strict JSON with these keys and types:\n- summary: string (<= 40 words)\n- decisions: string[]\n- notes: string[]\n- meeting_sentiment: one of {\"positive\",\"neutral\",\"negative\"}\n- tasks: array of objects {\n   description: string,\n   owner: string | \"TBD\",\n   deadline: ISO date string | \"TBD\",\n   sentiment: one of {\"positive\",\"neutral\",\"negative\"}\n}\nConstraints:\n- No prose, no markdown, no code fences.\n- If unknown, use empty list or \"TBD\".\n- Infer dates only if clearly stated; otherwise \"TBD\".\nassistant"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "2e1d64e1-a23e-432b-b2f2-54b4491bf3be",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1608,
        192
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "b37e84e6-4015-40b7-bcfe-745f1332f156",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2336,
        -32
      ],
      "parameters": {
        "sendTo": "={{ $('Google Drive Trigger').item.json.lastModifyingUser.emailAddress }}",
        "message": "={{ $json.email_html }}",
        "options": {},
        "subject": "=Meeting Summary \u2014 {{$json.meeting_sentiment}} tone"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "496624bb-9827-4fb3-9bc2-355265216e15",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -432
      ],
      "parameters": {
        "width": 384,
        "height": 832,
        "content": "## \ud83e\udde9 AI Meeting Summary & Email Automation\n\nAutomatically turn meeting files (PDF or TXT) uploaded to Google Drive into structured summaries with decisions, notes, and action items \u2014 then email them beautifully formatted via Gmail.\n\n---\n\n### \ud83e\udde0 How It Works\n1. Watch a Google Drive folder for new files  \n2. Extract text (from PDF or TXT)  \n3. Send content to OpenAI GPT-4o-mini  \n4. Parse and group tasks by sentiment  \n5. Build an HTML summary  \n6. Email it automatically\n\n---\n\n### \u2699\ufe0f Setup\n1. Connect Google Drive, OpenAI, and Gmail credentials  \n2. Point the Drive Trigger to your \u201cMeetings\u201d folder  \n3. Paste the system prompt into the AI node  \n4. Set Gmail message field to `{{$json.email_html}}`  \n5. Drop a file in the folder \u2014 everything runs end-to-end!\n\n---\n\n\ud83d\udca1 *Perfect for team syncs, standups, sprint reviews, or client calls.*\n"
      },
      "typeVersion": 1
    },
    {
      "id": "309556b1-a5ca-498c-a5ad-9d9024e13147",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        360,
        -496
      ],
      "parameters": {
        "color": 6,
        "width": 432,
        "height": 624,
        "content": "## Google Drive Trigger + File Routing\n\n**Watch New Files (Meeting Notes Folder)**  \nStarts the workflow whenever a new file appears in your Drive folder.\n\n**Check File Type (PDF or Text)**  \nRoutes files by MIME type:  \n- \u2705 `application/pdf` \u2192 PDF extraction path  \n- \u2705 `text/plain` \u2192 TXT extraction path  \n\n---\n\n\ud83d\udca1 *Keeps the workflow flexible for multiple file formats.*\n"
      },
      "typeVersion": 1
    },
    {
      "id": "9b04ab30-9935-4db6-b2fb-029567ed167e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        -448
      ],
      "parameters": {
        "color": 6,
        "width": 352,
        "height": 416,
        "content": "## AI Summarization\n\nThis step is where the AI does all the heavy lifting.  \nIt takes the extracted meeting text and uses **OpenAI GPT-4o-mini** to:\n\n- Read through the entire meeting transcript  \n- Identify and summarize key decisions and discussion points  \n- Extract important notes and insights  \n- Detect overall **meeting sentiment** (positive, neutral, or negative)  \n- Generate structured **tasks** with owners, deadlines, and sentiment tags  \n\nThe output is a **strict JSON object**, which ensures the next nodes can automatically parse, format, and send the results without manual cleanup.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a4d2bdfa-8b93-423b-a70d-06386279e994",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1840,
        112
      ],
      "parameters": {
        "color": 6,
        "height": 384,
        "content": "##  Validate & Structure Output\n\n**Normalize AI Output (Code Node)**  \n- Parses JSON safely  \n- Groups tasks by sentiment (`positive`, `neutral`, `negative`)  \n- Builds a `totals` object for quick statistics  \n- Prevents null/undefined errors in next step\n\n---\n\n\ud83d\udca1 *Ensures every summary email is structured and reliable.*\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b5ed5b24-bcac-4514-8ab1-64b52785034a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        -368
      ],
      "parameters": {
        "color": 6,
        "width": 256,
        "height": 320,
        "content": "## Generate HTML Summary\n\n**Generate HTML Report (Code Node)**  \nConverts the normalized JSON into a professional email-ready layout.\n\nIncludes:\n- \ud83d\udcdd Summary  \n- \ud83d\udca1 Key Decisions  \n- \ud83d\uddd2\ufe0f Notes  \n- \u2705 / \u26aa / \u274c grouped tasks  \n- Inline CSS for Gmail readability\n\n---\n\n\ud83d\udcc4 Output variable: `email_html`\n"
      },
      "typeVersion": 1
    },
    {
      "id": "49eb2ca5-6fb7-4659-adf6-bbbaac5c5bce",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        -128
      ],
      "parameters": {
        "color": 6,
        "width": 320,
        "height": 352,
        "content": "##  Send AI-Generated Report\n\n**Send AI Meeting Summary Email (Gmail)**  \n- **Email Type:** HTML  \n- **Message:** `{{$json.email_html}}`  \n- **Subject Example:**  \n  `AI Summary \u2013 {{$json.meeting_sentiment}} | \u2705{{$json.totals.positive}} \u26aa{{$json.totals.neutral}} \u274c{{$json.totals.negative}}`\n\n---\n\n\ud83d\udcec *Delivers the final meeting digest automatically to your inbox.*\n"
      },
      "typeVersion": 1
    },
    {
      "id": "83c30f5d-e519-4628-a42b-d278a0a3679c",
      "name": "Data Validation",
      "type": "n8n-nodes-base.code",
      "position": [
        1888,
        -32
      ],
      "parameters": {
        "jsCode": "/**\n * Robust parser for different upstream shapes:\n * - n8n AI Agent:        item.json.output (string)\n * - OpenAI Chat (HTTP):  item.json.body.choices[0].message.content\n * - OpenAI Chat (direct):item.json.choices[0].message.content\n * - Already an object:   item.json (if the LLM node already parsed JSON)\n */\nconst outputs = [];\n\nfor (const item of items) {\n  let content;   // string with JSON\n  let data;      // parsed object\n\n  // 1) Try common shapes\n  if (typeof item.json?.output === 'string') {\n    // n8n \"AI Agent\" node output\n    content = item.json.output;\n  } else if (typeof item.json?.choices?.[0]?.message?.content === 'string') {\n    // direct Chat Completions shape\n    content = item.json.choices[0].message.content;\n  } else if (typeof item.json?.body?.choices?.[0]?.message?.content === 'string') {\n    // HTTP Request node -> body -> choices\n    content = item.json.body.choices[0].message.content;\n  } else if (typeof item.json === 'string') {\n    // just a raw string\n    content = item.json;\n  } else if (typeof item.json === 'object' && item.json !== null && (\n             item.json.summary || item.json.tasks || item.json.decisions || item.json.notes)) {\n    // already parsed into an object with expected keys\n    data = item.json;\n  }\n\n  // 2) If we still need to parse, do it\n  if (!data) {\n    if (typeof content !== 'string') {\n      throw new Error('Could not locate LLM text output in upstream node. Check: output, choices[0].message.content, or body.choices...');\n    }\n    try {\n      data = JSON.parse(content);\n    } catch {\n      throw new Error('LLM did not return valid JSON. Verify the system prompt and response_format=json_object.');\n    }\n  }\n\n  // 3) Normalize the schema\n  if (typeof data.summary !== 'string') data.summary = '';\n  if (!Array.isArray(data.decisions)) data.decisions = [];\n  if (!Array.isArray(data.notes)) data.notes = [];\n  if (!['positive','neutral','negative'].includes(data.meeting_sentiment)) {\n    data.meeting_sentiment = 'neutral';\n  }\n  if (!Array.isArray(data.tasks)) data.tasks = [];\n\n  data.tasks = data.tasks.map(t => ({\n    description: t?.description ?? '',\n    owner: t?.owner ?? 'TBD',\n    deadline: t?.deadline ?? 'TBD',\n    sentiment: ['positive','neutral','negative'].includes(t?.sentiment) ? t.sentiment : 'neutral'\n  }));\n\n  // Optional grouped and totals\n  const grouped = { positive: [], neutral: [], negative: [] };\n  for (const t of data.tasks) grouped[t.sentiment].push(t);\n  data.tasks_grouped = grouped;\n  data.totals = {\n    positive: grouped.positive.length,\n    neutral: grouped.neutral.length,\n    negative: grouped.negative.length\n  };\n\n  outputs.push({ json: data });\n}\n\nreturn outputs;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4214d550-f3f0-40e0-87dc-3aab71ad931b",
      "name": "Data Preparation",
      "type": "n8n-nodes-base.code",
      "position": [
        2112,
        -32
      ],
      "parameters": {
        "jsCode": "// This Code node expects your previous node to output ONE item\n// with keys: summary, decisions[], notes[], meeting_sentiment,\n// tasks_grouped{positive[],neutral[],negative[]}, totals{...}.\n\n// helper funcs\nconst esc = (s) => String(s ?? '')\n  .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')\n  .replace(/\"/g,'&quot;').replace(/'/g,'&#39;');\n\nconst li = (s) => `<li>${esc(s)}</li>`;\nconst liTask = (t) =>\n  `<li><strong>${esc(t.description)}</strong> \u2014 ${esc(t.owner)} <em>(Deadline: ${esc(t.deadline)})</em></li>`;\n\n// read current item\nconst d = $json;\n\n// fallbacks if any section is missing\nconst decisions = Array.isArray(d.decisions) ? d.decisions : [];\nconst notes     = Array.isArray(d.notes) ? d.notes : [];\nconst tg        = d.tasks_grouped || { positive: [], neutral: [], negative: [] };\nconst totals    = d.totals || {\n  positive: tg.positive?.length ?? 0,\n  neutral:  tg.neutral?.length  ?? 0,\n  negative: tg.negative?.length ?? 0\n};\n\n// build section HTML safely\nconst decisionsHtml = decisions.map(li).join('');\nconst notesHtml     = notes.map(li).join('');\nconst posHtml       = (tg.positive || []).map(liTask).join('');\nconst neuHtml       = (tg.neutral  || []).map(liTask).join('');\nconst negHtml       = (tg.negative || []).map(liTask).join('');\n\n// MINIMAL, COMPATIBLE HTML (no handlebars, no fancy CSS needed)\nconst html = `<!DOCTYPE html>\n<html>\n  <body style=\"font-family: Arial, Helvetica, sans-serif; color:#1f2937; line-height:1.55; margin:0; padding:16px;\">\n    <h2 style=\"margin:0 0 8px 0;\">\ud83d\udcdd Meeting Summary</h2>\n    <p style=\"margin:0 0 8px 0;\">${esc(d.summary || '')}</p>\n\n    <p style=\"margin:8px 0;\">\n       <strong>Positive:</strong> ${totals.positive}\n      &nbsp;\u2022&nbsp; <strong>Neutral:</strong> ${totals.neutral}\n      &nbsp;\u2022&nbsp; <strong>Negative:</strong> ${totals.negative}\n    </p>\n\n    ${decisionsHtml ? `\n      <h3 style=\"margin:16px 0 6px;\">\ud83d\udca1 Key Decisions</h3>\n      <ul style=\"margin:0 0 12px 18px;\">${decisionsHtml}</ul>\n    ` : ''}\n\n    ${notesHtml ? `\n      <h3 style=\"margin:16px 0 6px;\">\ud83d\uddd2\ufe0f Notes</h3>\n      <ul style=\"margin:0 0 12px 18px;\">${notesHtml}</ul>\n    ` : ''}\n\n    ${posHtml ? `\n      <h3 style=\"margin:16px 0 6px;\">\u2705 Positive Tasks</h3>\n      <ul style=\"margin:0 0 12px 18px;\">${posHtml}</ul>\n    ` : ''}\n\n    ${neuHtml ? `\n      <h3 style=\"margin:16px 0 6px;\">\u26aa Neutral Tasks</h3>\n      <ul style=\"margin:0 0 12px 18px;\">${neuHtml}</ul>\n    ` : ''}\n\n    ${negHtml ? `\n      <h3 style=\"margin:16px 0 6px;\">\u274c Negative Tasks</h3>\n      <ul style=\"margin:0 0 12px 18px;\">${negHtml}</ul>\n    ` : ''}\n\n    <p style=\"font-size:12px; color:#6b7280; margin-top:16px;\">Sent by n8n \u2022 AI Meeting Assistant</p>\n  </body>\n</html>`;\n\n// output same data + ready-to-send html\nreturn [{ json: { ...d, email_html: html } }];\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "dea36ea5-7b21-4251-87b9-e9d64ed31e7b",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Download file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download file1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Data Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download file": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download file1": {
      "main": [
        [
          {
            "node": "Extract from File1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data Validation": {
      "main": [
        [
          {
            "node": "Data Preparation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Data Preparation": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File1": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

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

Pro

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

About this workflow

Project managers, AI builders, and teams who want structured, automated meeting summaries with zero manual work.

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Transcript Evalu8r V2 is a robust browser-based transcript analysis tool powered by Deepgram’s speech-to-text API and built into an n8n workflow template. This release introduces full in-browser audio

Google Drive Trigger, HTTP Request, Agent +5
AI & RAG

Transcript Evalu8r is an AI-powered transcript analysis workflow that automates the processing, visualization, and evaluation of transcribed conversations. This n8n workflow template is designed to he

Google Drive Trigger, HTTP Request, Agent +5
AI & RAG

Watches a Google Drive folder for new (scanned) invoices. Each new file automatically triggers the workflow. Downloads and processes each invoice through OCR Space to extract the text. Extracts the co

HTTP Request, Google Drive, Google Sheets Tool +5
AI & RAG

Monthly Invoice Summarizer. Uses googleDriveTrigger, googleDrive, lmChatOpenAi, outputParserStructured. Event-driven trigger; 28 nodes.

Google Drive Trigger, Google Drive, OpenAI Chat +6
AI & RAG

Automated invoice processing pipeline that extracts data from PDF invoices, uses AI Agent for intelligent expense categorization, generates XML for accounting systems, and routes high-value invoices f

Google Drive Trigger, Google Drive, OpenAI Chat +5