teachyou.ai academy
← All posts
n8n

n8n for Meeting Notes: Transcription, Summary and Action Items

Ira Menon · Jun 7, 2026 · 15 min read

Why meeting notes are the perfect first automation

Every team has the same ritual after a call ends. Someone scrolls back through their memory, tries to reconstruct who agreed to do what, and fires off a Slack message that says "recapping our discussion" three hours after everyone has already forgotten half of it. Meeting notes are unglamorous, but they are also one of the highest-leverage things you can automate, because the cost of skipping them compounds. A missed action item today becomes a missed deadline next week, and a missed deadline becomes a client escalation the week after that.

n8n is a good fit for this problem because meeting-notes automation is not really one task. It is a pipeline: get the audio or transcript, clean it up, summarize it, pull out decisions and owners, and route the output to the right places (a Notion database, a Slack channel, an email to attendees). Each of those steps is a node. You are not writing a monolithic script that breaks the moment one API changes shape — you are wiring together small, replaceable blocks that you can debug one at a time.

This article walks through building that pipeline end to end: capturing the meeting recording, transcribing it, generating a structured summary with action items, and delivering it wherever your team actually looks for information. Along the way we will cover the traps that make these workflows flaky in production — timeouts, malformed JSON from LLMs, and speaker attribution — and how to design around them.

Mapping the pipeline before touching a single node

Before opening the n8n canvas, it helps to sketch the shape of the workflow on paper. A meeting-notes pipeline usually has five stages:

  1. Trigger — something starts the workflow. This could be a webhook fired by your meeting tool (Zoom, Google Meet via a recall bot, or a manual upload), a scheduled poll of a shared drive folder, or a simple form submission where someone drops in an audio file.
  2. Acquire the transcript — either you already have a transcript (from Zoom's built-in captions, for example) or you need to generate one from an audio/video file using a speech-to-text service.
  3. Structure the raw text — long transcripts are noisy. Filler words, false starts, and cross-talk need to be normalized before you hand the text to a summarization step.
  4. Summarize and extract action items — this is where an LLM node does the heavy lifting, turning a wall of text into a short summary, a decisions list, and a table of action items with owners and due dates.
  5. Distribute — push the output to Notion, Slack, email, or all three, and optionally create calendar reminders or task-tracker tickets for each action item.

Thinking in these five stages matters because it tells you where to put your error handling. Stage 2 (transcription) is where external APIs are most likely to time out on long files. Stage 4 (LLM extraction) is where you need strict output formatting so downstream nodes do not choke on malformed JSON. Everything else is comparatively low-risk plumbing.

Getting the recording into n8n

Most teams already have their meeting audio somewhere — a Zoom cloud recording, a Google Meet recording saved to Drive, or an audio file dropped into a shared folder by a bot like a note-taker app. The trigger node depends on where that file lands.

If you are using Zoom, the cleanest approach is a webhook trigger fired on the recording.completed event. Zoom's webhook payload includes a download URL for the recording, which you fetch with an HTTP Request node using the temporary download token Zoom provides.

// Function node: extract the recording download URL and access token
const payload = $input.first().json;
const recordingFiles = payload.payload.object.recording_files;

const audioFile = recordingFiles.find(
  (file) => file.file_type === "M4A" || file.file_type === "MP4"
);

if (!audioFile) {
  throw new Error("No audio/video recording found in webhook payload");
}

return [
  {
    json: {
      downloadUrl: `${audioFile.download_url}?access_token=${payload.download_token}`,
      meetingTopic: payload.payload.object.topic,
      meetingId: payload.payload.object.id,
      startTime: payload.payload.object.start_time,
    },
  },
];

If your source is Google Meet, you generally do not get a direct webhook for recording completion, so a Google Drive trigger node watching a specific "Meet Recordings" folder works better. Poll every 5-10 minutes rather than every minute — Drive recordings can take a while to finish processing after a call ends, and polling too aggressively just wastes executions on files that are not ready yet.

For teams that use a note-taking bot to join calls automatically, that bot usually exposes its own webhook or REST API once a transcript is ready, which simplifies this stage considerably since you skip the audio step entirely and start at stage 3.

Transcribing the audio

If your source already gives you a transcript (Zoom's automated captions, for instance), you can skip straight to text cleanup. But a lot of teams standardize on an external speech-to-text API because it produces better speaker labels and timestamps than in-app captions.

The pattern in n8n is: download the audio file with an HTTP Request node, then pass it as binary data to your transcription provider's API in a second HTTP Request node (or a dedicated community node if one exists for your provider). Because audio files for long meetings can be sizeable, set the HTTP Request node's timeout generously — 60 seconds is often too short for a 90-minute call being uploaded and transcribed.

// Function node: prepare the transcription request body
const binaryData = $binary.data;

return [
  {
    json: {
      model: "whisper-large-v3",
      language: "en",
      response_format: "verbose_json",
      timestamp_granularities: ["segment"],
    },
    binary: {
      data: binaryData,
    },
  },
];

Two details matter here. First, request verbose_json or an equivalent structured response rather than plain text — you want segment-level timestamps so you can later correlate action items back to a point in the recording if someone needs to double-check what was actually said. Second, if your provider supports diarization (speaker separation), turn it on even though it usually costs a bit more per minute. Speaker labels are what let your summary say "Priya agreed to send the pricing doc" instead of "someone agreed to send the pricing doc," and that distinction is the entire point of an action-items list.

For very long recordings, some transcription APIs impose a hard duration or file-size limit. If you regularly run meetings longer than an hour, add a splitting step: use an FFmpeg-based node or a Code node calling a conversion utility to chunk the audio into 20-minute segments, transcribe each chunk separately, then concatenate the transcripts in order before moving to summarization. This adds complexity, but it is much better than a workflow that silently fails on your longest, often most important, meetings.

Cleaning and structuring the raw transcript

Raw transcripts are messy. They contain filler words, repeated phrases, and — if you have diarization — speaker labels like SPEAKER_00 and SPEAKER_01 instead of real names. Before summarizing, run the transcript through a Code node that does light cleanup and, if possible, maps generic speaker IDs to actual names using the meeting's attendee list.

// Code node: merge segments and map speaker IDs to real names
const segments = $input.first().json.segments;
const attendees = $('Get Attendees').first().json.attendees;

// Simple mapping: assumes speakers appear in join order.
// Replace with a voice-print match if your provider supports it.
const speakerMap = {};
attendees.forEach((name, index) => {
  speakerMap[`SPEAKER_${String(index).padStart(2, "0")}`] = name;
});

const lines = segments.map((seg) => {
  const speaker = speakerMap[seg.speaker] || seg.speaker || "Unknown";
  return `${speaker}: ${seg.text.trim()}`;
});

const fullTranscript = lines.join("\n");

return [{ json: { fullTranscript, attendeeCount: attendees.length } }];

The speaker-mapping-by-join-order trick is a rough heuristic, not a guarantee — if attendees join out of order relative to when the diarization engine first hears them speak, names will be wrong. If accurate speaker attribution matters a lot for your use case (client calls, performance reviews), look for a transcription provider that lets you register voice prints per person, or accept manual correction as a fallback and design your Slack/email output so a human can quickly fix a mislabeled name before it goes out widely.

Also worth doing at this stage: strip the transcript down if it is extremely long. Most LLMs handle meeting-length transcripts fine within a large context window, but if you are chaining multiple LLM calls (summary, then action items, then a Slack-friendly version), you will save cost and latency by summarizing once and reusing that output for the smaller outputs, rather than sending the full raw transcript to every downstream node.

Summarizing and extracting action items with an LLM node

This is the core of the workflow. Use n8n's LLM node (or an HTTP Request node against your model provider) with a carefully constrained prompt that forces structured output. The single biggest failure mode in production meeting-notes workflows is an LLM that returns almost-JSON — an extra sentence before the object, a trailing comma, or markdown code fences around the JSON — which then breaks whatever node tries to parse it downstream.

You are a meeting-notes assistant. You will be given a raw meeting transcript
with speaker labels. Produce ONLY a JSON object, with no commentary, no
markdown fences, and no text outside the JSON, matching this exact shape:

{
  "summary": "3-5 sentence plain-English summary of the meeting",
  "keyDecisions": ["decision 1", "decision 2"],
  "actionItems": [
    {
      "task": "short description of what needs to be done",
      "owner": "name of the person responsible, or 'Unassigned'",
      "dueDate": "YYYY-MM-DD if mentioned, otherwise null"
    }
  ],
  "openQuestions": ["unresolved question 1"]
}

Transcript:
{{ $json.fullTranscript }}

Even with an explicit instruction to skip markdown fences, models sometimes wrap the JSON in triple backticks anyway. Add a Code node right after the LLM call that strips fences defensively and parses with a fallback, rather than trusting the raw output.

// Code node: robustly parse LLM output that may include markdown fences
const raw = $input.first().json.text ?? $input.first().json.content ?? "";

function extractJson(text) {
  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
  const candidate = fenced ? fenced[1] : text;
  return JSON.parse(candidate.trim());
}

let parsed;
try {
  parsed = extractJson(raw);
} catch (err) {
  // Fall back to a minimal shape so downstream nodes don't crash the whole run
  parsed = {
    summary: "Automated summary failed to parse. Review transcript manually.",
    keyDecisions: [],
    actionItems: [],
    openQuestions: [],
    parseError: err.message,
  };
}

return [{ json: parsed }];

That fallback branch is not optional polish — it is what keeps one bad LLM response from taking down the entire run and leaving your team with nothing instead of an imperfect-but-usable summary. Pair it with an IF node downstream that checks for parseError and routes to a "needs manual review" Slack alert instead of silently publishing broken data to Notion.

It is also worth setting the LLM node's temperature low (0.1-0.3) for this task. You are not looking for creative variation across runs — you want the same meeting to produce essentially the same structured extraction whether you run it once or three times, which matters both for consistency and for debugging when something goes wrong.

Handling long or multi-topic meetings

A one-hour, four-topic strategy meeting does not compress well into a single flat action-items list — action items from the budget discussion get mixed in with action items from the hiring discussion, and the summary reads like a run-on paragraph. For meetings with a known agenda, it is worth adding an intermediate step that segments the transcript by topic before summarizing.

One practical approach: if you send calendar invites with an agenda in the description, pull that agenda into the workflow via a Calendar node early on, and pass it into the summarization prompt as context so the model organizes its output under the same headings your team already expects.

The meeting agenda was:
1. Q3 budget review
2. Hiring plan for engineering
3. Customer escalation follow-up

Organize your summary and action items under these three headings. If a
discussion doesn't map to any heading, add it under "Other topics".

This one change — giving the model the agenda as grounding — noticeably improves output quality for longer meetings, because it stops the summarization step from treating a 90-minute transcript as one undifferentiated blob.

Routing action items to Notion, Slack, and calendars

Once you have clean, structured JSON, the distribution stage is mostly about fan-out. A Split Out node on the actionItems array lets you loop through each item and create one row per action item in a Notion database (with columns for Task, Owner, Due Date, Source Meeting, and Status defaulting to "Not Started"). This is far more useful than dumping all action items into a single paragraph in a Notion page, because a database view lets each owner filter to "my open items" across every meeting, not just the one they just attended.

// Function node: format each action item for the Notion Create Page node
const item = $json;

return [
  {
    json: {
      task: item.task,
      owner: item.owner,
      dueDate: item.dueDate,
      status: "Not Started",
      sourceMeeting: $('Webhook').first().json.meetingTopic,
      meetingDate: $('Webhook').first().json.startTime,
    },
  },
];

For the human-readable recap, a Slack message to the meeting's channel works better than email for most teams, because it is visible without anyone opening their inbox, and it threads naturally if someone wants to correct an item or add context. Format the Slack message with the summary up top, decisions as a short bulleted list, and action items as a checklist using Slack's markdown so people can visually track what's done.

*Meeting Recap: {{ $json.meetingTopic }}*

{{ $json.summary }}

*Key Decisions*
{{ $json.keyDecisions.map(d => `• ${d}`).join('\n') }}

*Action Items*
{{ $json.actionItems.map(a => `☐ ${a.task} — _${a.owner}_${a.dueDate ? ' (due ' + a.dueDate + ')' : ''}`).join('\n') }}

If your team runs on email instead, a simple HTML email node with the same structure works, sent to all meeting attendees plus anyone tagged as an owner who might not have attended. Do not skip this — action items often get assigned to people who were mentioned but weren't actually on the call, and they need to find out some other way than stumbling across a Notion row three days later.

Error handling and monitoring for a workflow that runs unattended

A meeting-notes workflow is exactly the kind of automation that runs quietly in the background for months, which means when it breaks, it breaks silently unless you have built in monitoring. A few practices that pay off:

  • Wrap the transcription and LLM HTTP Request nodes with error-output enabled, and route failures to a dedicated "automation failed" Slack channel with the meeting name and error message, rather than letting the whole execution just vanish into n8n's failed-executions log where nobody checks it.
  • Add a timeout guard on the transcription step. Long audio files can hang. Set a reasonable node timeout and, on timeout, send a notification rather than leaving the workflow stuck in a "running" state indefinitely.
  • Log every run's output to a simple spreadsheet or database table (meeting name, date, success/failure, parse errors) even if you also push to Notion. This becomes invaluable three months in when someone asks "did we actually get notes from the March 12 board meeting?" and you can check a log instead of digging through Slack history.
  • Set up a weekly digest node that queries your Notion action-items database for anything with status "Not Started" and a due date more than a week old, and posts a gentle nudge to the relevant channel. This closes the loop — capturing action items is only half the value; making sure they don't silently expire is the other half.

Privacy and access considerations

Meeting transcripts often contain sensitive information — compensation discussions, client details, unreleased product plans — so treat this pipeline with the same care as any other system handling confidential data. Restrict who can view the n8n workflow's credentials, use scoped API keys for your transcription and LLM providers rather than shared organizational keys, and be deliberate about which Slack channels or Notion databases receive the output. Not every meeting recap belongs in a company-wide channel. If you handle recordings for calls involving clients or candidates, check your provider's data-retention settings so raw audio isn't kept longer than needed after transcription completes, and confirm your LLM provider's data usage policy if you're sending transcripts to a third-party API rather than a self-hosted model.

Putting it all together

The finished workflow looks roughly like this: a trigger fires when a recording is ready, an HTTP Request node downloads the audio, a transcription API call produces a diarized transcript, a Code node cleans it up and maps speaker names, an LLM node extracts a structured summary with action items, a defensive parsing step guards against malformed JSON, and a fan-out stage pushes the results to Notion, Slack, and anyone who needs a due-date nudge later. None of the individual pieces are exotic — it's the composition, plus the error handling at each seam, that turns this from a demo into something your team can actually rely on every single week.

Start small if you're building this for the first time. Get the transcription and summary working reliably for one recurring meeting before you add speaker mapping, agenda segmentation, and multi-channel distribution. Each of those refinements is a small, addable node once the core pipeline is solid — but if you try to build the full version on day one, you'll spend most of your debugging time guessing which of six new features broke the run.

If you want a structured, hands-on path through building agentic workflows like this one — including patterns for tool calling, structured output validation, and multi-step orchestration beyond just meeting notes — check out the n8n AI Agent Tutorial course on teachyou.ai. It walks through building production-grade n8n automations from first principles, with the same emphasis on error handling and reliability covered here.