DEV Community

Mateo Ruiz
Mateo Ruiz

Posted on Originally published at itpathsolutions.com

Automate Meeting Notes, Action Items & Task Creation with n8n and AI

Post-meeting documentation is one of the most consistently skipped steps in any team's workflow. Writing a summary, extracting action items, assigning owners, converting vague deadlines into real dates, and entering tasks one-by-one into a project management tool takes 15–30 minutes per meeting — and it's the first thing that slips when the calendar fills up.

This post walks through a production-ready n8n workflow that automates the entire post-meeting pipeline — Google Drive trigger, Groq Whisper transcription, LLM-based summarisation and extraction, natural language date parsing, Gmail summary email, and Trello task card creation — with a free downloadable workflow JSON at the end.


Architecture

┌──────────────────────────────────────────────────────────────────┐
│              n8n AI Meeting Notes Workflow                        │
│                                                                  │
│  [Google Drive Trigger] ← watches /meeting-recordings/ folder   │
│           ↓                                                      │
│  [Google Drive: Download File Binary]                            │
│           ↓                                                      │
│  [Groq: Whisper Transcription] → raw transcript text            │
│           ↓                                                      │
│  [Function Node] → extract clean transcript string              │
│           ↓                                                      │
│  [Groq LLM: Summarise + Extract] → structured JSON output      │
│           ↓                                                      │
│  [Function Node] → parse JSON + convert natural dates           │
│           ↓                                                      │
│  ┌─────────────────────────────────────┐                        │
│  │         SPLIT OUTPUT                │                        │
│  │                                     │                        │
│  │  [Gmail: Send Summary Email]        │                        │
│  │  [Trello: Create Card per Action]   │                        │
│  └─────────────────────────────────────┘                        │
│           ↓                                                      │
│  [Error Branch: Log + Alert on failure]                         │
└──────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Stack

Tool Role Swappable With
n8n (self-hosted or Cloud) Orchestration
Google Drive Recording trigger + file source Dropbox, OneDrive
Groq Whisper Audio transcription OpenAI Whisper, AssemblyAI
Groq LLM Summarisation + extraction OpenAI GPT-4o, Claude, Gemini
Gmail Summary email distribution Outlook, Slack notification
Trello Task card creation Asana, ClickUp, Linear, Monday

Node-by-Node Breakdown

1. Google Drive Trigger

Watch a specific folder for new file creation events:

Folder: /meeting-recordings/
Event: File Created
Poll interval: ~1 minute
Enter fullscreen mode Exit fullscreen mode

Folder structure recommendation:

/meeting-recordings/
  /2026-09-10-product-standup.mp4
  /2026-09-11-client-call-acme.mp3
  /2026-09-12-sprint-review.mp4
Enter fullscreen mode Exit fullscreen mode

Why file creation only: Re-saving or renaming an existing file inside the folder must not re-trigger the workflow. Creation events only.

Supported formats: The workflow handles audio files (.mp3, .wav, .m4a) and video files with audio tracks (.mp4, .mov, .webm) — Groq Whisper processes only the audio portion. Test your recording format before going live.


2. Download File from Google Drive

// Google Drive node config
Operation: Download File
File ID: {{ $json.id }}
Binary Property: recordingFile
Enter fullscreen mode Exit fullscreen mode

This downloads the actual file binary into the workflow for the transcription step.


3. Groq Whisper Transcription

Send the file binary to Groq's Whisper endpoint:

// HTTP Request node — Groq Whisper API
Method: POST
URL: https://api.groq.com/openai/v1/audio/transcriptions
Headers:
  Authorization: Bearer {{ $credentials.groqApiKey }}
  Content-Type: multipart/form-data

Body (form-data):
  file: {{ $binary.recordingFile }}
  model: whisper-large-v3
  response_format: json
  language: en  // optional — remove for auto-detection
Enter fullscreen mode Exit fullscreen mode

Response structure:

{
  "text": "Full transcript text here..."
}
Enter fullscreen mode Exit fullscreen mode

Audio quality note: Groq Whisper's accuracy depends heavily on audio quality. Background noise, overlapping speakers, and low-quality microphones all degrade the transcript — which then degrades every downstream extraction. There's no prompt tuning that compensates for a bad recording.


4. Extract Clean Transcript

// Function Node — extract transcript string
const transcriptData = $input.first().json;
const transcript = transcriptData.text || '';

if (!transcript || transcript.trim().length < 50) {
  throw new Error('Transcript too short or empty — check audio file quality');
}

return [{
  json: {
    transcript: transcript.trim(),
    char_count: transcript.length,
    estimated_words: Math.round(transcript.split(' ').length)
  }
}];
Enter fullscreen mode Exit fullscreen mode

The character count is useful for prompt token estimation — long meetings (60+ minutes) produce long transcripts that may approach context window limits depending on your LLM provider.


5. Groq LLM — Summarise and Extract

This is the core intelligence step. The LLM receives the transcript and returns structured data.

System Prompt

You are an expert meeting analyst. Extract structured information from the meeting transcript below.

Return ONLY valid JSON  no preamble, no markdown fences, no explanation:
{
  "summary": "2-3 sentence overview of the meeting's main purpose and outcomes",
  "decisions": [
    "Decision 1 made during the meeting",
    "Decision 2 made during the meeting"
  ],
  "action_items": [
    {
      "task": "Clear description of what needs to be done",
      "owner": "Person's name or 'Unassigned' if unclear",
      "due_date": "Exact phrase used in meeting (e.g. 'by Friday', 'end of next week', 'tomorrow', or 'No deadline mentioned')",
      "priority": "high|medium|low"
    }
  ],
  "meeting_type": "standup|client_call|sprint_review|sales_call|other",
  "attendees_mentioned": ["Name 1", "Name 2"]
}

Rules:
- Only extract action items that were explicitly committed to, not casual suggestions
- If ownership is unclear, set owner to "Unassigned"
- Capture due_date as the exact phrase used  the next step will convert it to a real date
- If no decisions were made, return an empty decisions array
- Keep the summary factual, not interpretive
Enter fullscreen mode Exit fullscreen mode

Key settings:

  • Temperature: 0.1 — consistent extraction matters more than creative variation
  • Model: llama-3.1-70b-versatile (Groq) or swap for GPT-4o / Claude Sonnet for better reasoning on complex meetings

6. Parse JSON + Convert Natural Language Dates

This is the step most similar workflows skip — and it's the one that makes Trello cards actually usable.

// Function Node — parse + convert dates
const raw = $input.first().json.content || $input.first().json.choices?.[0]?.message?.content || '';

// Strip markdown fences if model added them
const clean = raw.replace(/^```
{% endraw %}
json\s*/i, '').replace(/\s*
{% raw %}
```$/, '').trim();

let parsed;
try {
  parsed = JSON.parse(clean);
} catch(e) {
  throw new Error(`LLM JSON parse failed. Raw output: ${raw.substring(0, 400)}`);
}

// Convert natural language dates to real ISO dates
function parseDueDate(phrase) {
  if (!phrase || phrase === 'No deadline mentioned') return null;

  const today = new Date();
  const lower = phrase.toLowerCase().trim();

  // Named days
  const days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday'];
  const dayMatch = days.findIndex(d => lower.includes(d));
  if (dayMatch !== -1) {
    const targetDay = dayMatch;
    const currentDay = today.getDay();
    let daysAhead = targetDay - currentDay;
    if (daysAhead <= 0) daysAhead += 7; // next occurrence
    const result = new Date(today);
    result.setDate(today.getDate() + daysAhead);
    return result.toISOString().split('T')[0];
  }

  // Relative phrases
  if (lower.includes('tomorrow')) {
    const d = new Date(today); d.setDate(today.getDate() + 1);
    return d.toISOString().split('T')[0];
  }
  if (lower.includes('next week')) {
    const d = new Date(today); d.setDate(today.getDate() + 7);
    return d.toISOString().split('T')[0];
  }
  if (lower.includes('end of week') || lower.includes('eow')) {
    const d = new Date(today);
    const daysToFriday = (5 - today.getDay() + 7) % 7 || 7;
    d.setDate(today.getDate() + daysToFriday);
    return d.toISOString().split('T')[0];
  }
  if (lower.includes('end of month') || lower.includes('eom')) {
    const d = new Date(today.getFullYear(), today.getMonth() + 1, 0);
    return d.toISOString().split('T')[0];
  }
  if (lower.includes('end of day') || lower.includes('eod') || lower.includes('today')) {
    return today.toISOString().split('T')[0];
  }
  if (lower.includes('2 weeks') || lower.includes('two weeks')) {
    const d = new Date(today); d.setDate(today.getDate() + 14);
    return d.toISOString().split('T')[0];
  }

  // Try direct date parsing as fallback
  const attempted = new Date(phrase);
  if (!isNaN(attempted.getTime())) {
    return attempted.toISOString().split('T')[0];
  }

  return null; // Unknown phrase — leave date unset
}

// Convert all action item due dates
const processedItems = (parsed.action_items || []).map(item => ({
  ...item,
  due_date_raw: item.due_date,
  due_date_iso: parseDueDate(item.due_date)
}));

return [{
  json: {
    summary: parsed.summary,
    decisions: parsed.decisions || [],
    action_items: processedItems,
    meeting_type: parsed.meeting_type,
    attendees_mentioned: parsed.attendees_mentioned || [],
    total_action_items: processedItems.length
  }
}];
Enter fullscreen mode Exit fullscreen mode

Why this matters: Trello, Asana, ClickUp, and every other task tool require ISO date strings for due dates. Passing "by Friday" or "end of next week" fails silently — the card gets created with no due date. This parser bridges that gap.


7. Gmail — Send Summary Email

// Build formatted email body
const { summary, decisions, action_items, meeting_type } = $input.first().json;

const decisionsHtml = decisions.length > 0
  ? `<h3>Decisions Made</h3><ul>${decisions.map(d => `<li>${d}</li>`).join('')}</ul>`
  : '';

const actionItemsHtml = action_items.map(item => `
  <li>
    <strong>${item.task}</strong><br>
    Owner: ${item.owner} | 
    Due: ${item.due_date_iso || item.due_date_raw || 'TBD'} | 
    Priority: ${item.priority}
  </li>
`).join('');

const emailBody = `
<h2>Meeting Summary</h2>
<p>${summary}</p>

${decisionsHtml}

<h3>Action Items (${action_items.length})</h3>
<ul>${actionItemsHtml}</ul>

<hr>
<p style="color:#888;font-size:12px">Generated automatically by AI meeting notes workflow</p>
`;
Enter fullscreen mode Exit fullscreen mode

Configure the Gmail node with:

  • To: Attendee list (static or dynamic from a column/field)
  • Subject: Meeting Summary — {{ $now.format('YYYY-MM-DD') }} — {{ $json.meeting_type }}
  • Body: HTML body built above

8. Trello — Create Card Per Action Item

Use n8n's SplitInBatches node to iterate over each action item and create one Trello card per item:

// Trello node config per card
Name: {{ $json.task }}
Description: Owner: {{ $json.owner }}\nPriority: {{ $json.priority }}\nOriginal deadline phrase: {{ $json.due_date_raw }}
Due Date: {{ $json.due_date_iso }}  // ISO format: 2026-09-19
List ID: [your-trello-list-id]
Board ID: [your-trello-board-id]
Enter fullscreen mode Exit fullscreen mode

Unassigned items: If owner is "Unassigned", consider routing to a separate Trello list or adding a label so it's visible as needing an owner assignment.


9. Error Handling

The original workflow has no built-in error handling — add this before going live:

// Error branch function node
const errorDetails = {
  timestamp: new Date().toISOString(),
  pipeline: "AI Meeting Notes",
  failed_node: $input.first().json.failedNode || "unknown",
  error_message: $input.first().json.error?.message || "Unknown error",
  recording_file: $('Google Drive Trigger').first().json?.name || "unknown"
};
return [{ json: errorDetails }];
Enter fullscreen mode Exit fullscreen mode

After capturing: log to Google Sheets + send Gmail/Slack alert so silent failures are visible.

Common Failure Points

Failure Cause Handling
Whisper timeout Long recording (60+ min) Increase timeout to 120s, retry once
LLM JSON parse error Model added markdown fences Strip with regex, retry once
Empty transcript Poor audio / unsupported format Check file format, alert uploader
Trello 400 Invalid board/list ID Validate IDs in setup, log full error
Gmail auth error OAuth token expired Alert team, manual token refresh
Date parse returns null Unknown date phrase Card created with no due date — acceptable

Customisation Options

Swap Transcription Provider

Groq Whisper → OpenAI Whisper (/v1/audio/transcriptions, same API shape)
Groq Whisper → AssemblyAI (better speaker diarization for multi-person meetings)
Enter fullscreen mode Exit fullscreen mode

AssemblyAI is worth considering if your meetings involve multiple speakers and you want the transcript to label who said what — which improves ownership extraction in the LLM step.

Swap Task Tool

Tool n8n Node Change Required
Asana Asana node Map to task name, assignee, due_on
ClickUp ClickUp node Map to task name, assignee, due_date
Linear HTTP Request (Linear API) Map to title, assignee, dueDate
Notion Notion node Map to page title + database properties

The core logic doesn't change — only the output node and field mapping.

Add Approval Gate

For sensitive meetings (client calls, contract discussions, major decisions):

[LLM extraction complete]
       ↓
[Slack: Send summary for approval]
  "Does this summary look accurate? Approve / Edit"
       ↓
[n8n Wait Node] ← pauses workflow
       ↓
[Webhook: receives approval]
       ↓
[IF approved] → send email + create tasks
[IF rejected] → flag for manual edit
Enter fullscreen mode Exit fullscreen mode

Add Google Sheets Logging

[After email + tasks created]
       ↓
[Google Sheets: Append Row]
Columns: date | meeting_type | recording_file | summary | action_count | attendees
Enter fullscreen mode Exit fullscreen mode

Gives a searchable meeting archive and lets you track patterns (which meeting types produce the most action items, recurring blockers, etc.).

Handle Long Transcripts

For recordings over 45 minutes, transcripts can exceed LLM context windows. Chunk the transcript before sending:

// Split long transcripts into chunks with overlap
const MAX_CHARS = 12000;
const OVERLAP = 500;

if (transcript.length <= MAX_CHARS) {
  return [{ json: { transcript, chunks: 1 } }];
}

const chunks = [];
let start = 0;
while (start < transcript.length) {
  chunks.push(transcript.slice(start, start + MAX_CHARS));
  start += MAX_CHARS - OVERLAP;
}

// Process each chunk separately, then merge summaries
return chunks.map((chunk, i) => ({ json: { transcript: chunk, chunk_index: i } }));
Enter fullscreen mode Exit fullscreen mode

Limitations

Audio quality gates everything. There's no prompt tuning that compensates for a bad transcript. Improve recording quality at the source (good microphone, quiet room, one speaker at a time where possible).

Vague language produces vague action items. The LLM extracts what was said. "We should probably look into that" is not a firm commitment — but it might get extracted as one. A quick skim of the summary before treating it as official is still good practice.

No built-in error alerting in the base workflow. Add the error branch before relying on this daily. Silent failures are the hardest to catch.

Natural language date parsing covers common phrases but not all. Add patterns to the parser as you encounter phrases your team uses that aren't covered by the defaults.


Get the Free Workflow JSON

IT Path Solutions published the complete n8n workflow — Google Drive trigger, Groq Whisper transcription, LLM summarisation with structured output, natural language date parser, Gmail summary email, Trello task card creation, and the foundation for error handling — all pre-connected.

Import into any n8n instance, add credentials, run against one test recording first.

👉 Download the free AI meeting notes workflow JSON

Setup guide covers: Google Drive OAuth, Groq API key setup, Gmail OAuth, Trello board/list ID lookup, and what to check in the first few test runs.


Summary

The pipeline works because it handles every mechanical post-meeting step — transcription, summarisation, date conversion, email, task creation — automatically within minutes of a recording landing in the watched folder. The team gets a summary in their inbox and tasks in their board without anyone writing anything.

Add the error branch before going live. Test on a real recording first and adjust the extraction prompt if action item quality needs tuning.

Full guide and JSON: itpathsolutions.com/ai-meeting-notes-automation-n8n-workflow

Top comments (0)