n8n workflow: Zoom AI Meeting Assistant

Nodes:
name: OpenAI Chat Model
type: @n8n/n8n-nodes-langchain.lmChatOpenAi
parameters: model: gpt-4o
name: When clicking ‘Test workflow’
type: n8n-nodes-base.manualTrigger
name: No Recording/Transcript available
type: n8n-nodes-base.stopAndError
parameters: errorMessage: ={{ $json.error.cause.message }}
name: Zoom: Get data of last meeting
type: n8n-nodes-base.zoom
parameters: filters: type: scheduled
operation: getAll
returnAll: yes
authentication: oAuth2
name: Filter transcript URL
type: n8n-nodes-base.set
onError: continueRegularOutput
parameters: assignments: assignments: name: transcript_file
type: string
value: ={{ $json.recording_files.find(f => f.file_type === 'TRANSCRIPT').download_url }}
name: Filter: Only 1 item
type: n8n-nodes-base.splitInBatches
name: Zoom: Get transcript file
type: n8n-nodes-base.httpRequest
parameters: url: ={{ $json.transcript_file }}
authentication: predefinedCredentialType
nodeCredentialType: zoomOAuth2Api
name: Extract text from transcript file
type: n8n-nodes-base.extractFromFile
parameters: operation: text
name: Format transcript text
type: n8n-nodes-base.set
parameters: assignments: assignments: name: transcript
type: string
value: ={{ $json.data.split('\r\n\r\n').slice(1).map(block => {
    const lines = block.split('\r\n');
    return lines.slice(2).join(' ');
}).join('\n') }}
name: Zoom: Get participants data
type: n8n-nodes-base.httpRequest
parameters: url: =https://api.zoom.us/v2/past_meetings/{{ $('Filter: Last 24 hours').item.json.id }}/participants
authentication: predefinedCredentialType
nodeCredentialType: zoomOAuth2Api
name: Create meeting summary
type: @n8n/n8n-nodes-langchain.openAi
parameters: modelId: __rl: yes
mode: list
value: gpt-4o
cachedResultName: GPT-4O
messages: values: =Create a formal meeting minutes document from the following transcript and meeting details.

Meeting Date: {{ $('Zoom: Get data of last meeting').item.json.start_time }} // This needs to be formatted from the meeting details
Participants: {{ $json.participants.map(p => p.name + ' (' + p.user_email + ')').join(', ') }}

Transcript:
{{ $('Format transcript text').item.json.transcript }}

Please create the minutes in the following format:

Meeting on [Date]

Participants:
[List of participants with email addresses]

Summary of the Meeting:
[Brief and concise summary of the topics discussed]

Tasks:
- [Task] (Responsible: [Name])
- ...

Important Dates:
- [Date] ([Context])
- ...

name: Sort for mail delivery
type: n8n-nodes-base.set
parameters: assignments: assignments: name: subject
type: string
value: =Meeting summary: {{ $('Zoom: Get data of last meeting').item.json.topic }} on {{ $('Zoom: Get data of last meeting').item.json.start_time }}
name: =to
type: string
value: ={{ $('Zoom: Get participants data').item.json.participants[0].user_email }}
name: body
type: string
value: ={{ $json.message.content }}
name: Format to html
type: n8n-nodes-base.code
parameters: jsCode: const items = [];

for (const item of $input.all()) {
  const body = item.json.body;
  if (!body) continue;

  // Simple split approach
  const sections = body.split('\n\n');
  const title = sections[0].replace(/\*\*/g, '');
  const participants = sections[1].split('\n').slice(1).join('\n');
  const summary = sections[2].split('\n').slice(1).join('\n');
  const tasks = sections[3].split('\n').slice(1).join('\n');
  const dates = sections[4].split('\n').slice(1).join('\n');

  const html = `<html>
<body style="font-family: Arial, sans-serif; max-width: 800px; margin: 20px;">
<h1 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px;">${title}</h1>
<h2 style="color: #2c3e50; margin-top: 20px;">Participants:</h2>
<ul style="list-style-type: none; padding-left: 20px;">
${participants.split('\n').map(p => `<li>${p.replace('- ', '')}</li>`).join('\n')}
</ul>
<h2 style="color: #2c3e50; margin-top: 20px;">Meeting Summary:</h2>
<p style="margin-left: 20px;">${summary}</p>
<h2 style="color: #2c3e50; margin-top: 20px;">Tasks:</h2>
<ul style="margin-left: 20px;">
${tasks.split('\n').map(t => `<li>${t.replace('- ', '')}</li>`).join('\n')}
</ul>
<h2 style="color: #2c3e50; margin-top: 20px;">Important Dates:</h2>
<ul style="margin-left: 20px;">
${dates.split('\n').map(d => `<li>${d.replace('- ', '')}</li>`).join('\n')}
</ul>
</body>
</html>`;

  items.push({
    json: {
      html,
      to: item.json.to,
      subject: item.json.subject
    }
  });
}

return items;
name: Send meeting summary
type: n8n-nodes-base.emailSend
parameters: html: ={{ $json.html }}
subject: ={{ $json.subject }}
toEmail: ={{ $json.to }}
fromEmail: friedemann.schuetz@posteo.de
name: Create tasks
type: @n8n/n8n-nodes-langchain.toolWorkflow
parameters: name: create_task
schemaType: manual
workflowId: __rl: yes
mode: list
value: zSKQLEObdU9RiThI
cachedResultName: create_task
description: =Use this tool to create a task. 
For task creation use only action items for me Friedemann, don't use action items for other participants.
inputSchema: {
    "type": "object",
    "properties": {
      "items": {
        "type": "array",
        "description": "An array of tasks",
        "items": {
          "type": "object",
          "properties": {
            "name": {
              "type": "string",
              "description": "The name of the task"
            },
            "description": {
              "type": "string",
              "description": "A detailed description of the task"
            },
            "due_date": {
              "type": "string",
              "description": "Due Date"
            },
            "priority": {
              "type": "string",
              "description": "Priority. . Please capitalize first letter"
            },
            "project_name": {
              "type": "string",
              "description": "Name of the project. Word 'Project' shouldn't be included"
            }
          },
          "required": [
            "name",
            "description",
            "due_date",
            "priority"
          ],
          "additionalProperties": false
        }
      }
    },
    "required": [
      "items"
    ],
    "additionalProperties": false
}
specifyInputSchema: yes
name: Create tasks and follow-up call
type: @n8n/n8n-nodes-langchain.agent
parameters: text: =<system_prompt>

TODAY IS: {{ $now }}

YOU ARE A MEETING ASSISTANT FOR AUTOMATION IN N8N. YOUR TASK IS TO EFFICIENTLY AND PRECISELY PROCESS INFORMATION FROM ZOOM MEETINGS TO GENERATE TO-DOS AND SCHEDULE FOLLOW-UP MEETINGS. YOU HAVE ACCESS TO THE FOLLOWING DATA:

### INPUTS ###
- **MEETING TITLE**: {{ $('Zoom: Get data of last meeting').item.json.topic }}
- **PARTICIPANTS**: {{ $('Zoom: Get participants data').item.json.participants[0].name }}
- **TRANSCRIPT**: {{ $('Format transcript text').item.json.transcript }}

### YOUR TASKS ###
1. **CREATE TO-DOS**:
   - IDENTIFY TASKS AND TO-DOS IN THE TRANSCRIPT.
   - FORMULATE CLEAR, CONCRETE TASKS.
   - PASS THESE TASKS TO THE TOOL "Create tasks" TO SAVE THEM IN CLICKUP. 
   - DATA STRUCTURE:
     - **TASK DESCRIPTION**: Brief description of the task.
     - **ASSIGNED PERSON**: First name from the participant list.
     - **DUE DATE**: Use any date mentioned in the transcript; otherwise, set to "Not specified."

2. **CREATE MEETING**:
   - ANALYZE THE TRANSCRIPT TO IDENTIFY INFORMATION ABOUT THE NEXT MEETING (DATE, TIME, AND TOPIC).
   - PASS THIS INFORMATION TO THE TOOL "Create follow-up call."
   - DATA STRUCTURE:
     - **MEETING TITLE**: "Follow-up: [Meeting Title]"
     - **DATE AND TIME**: Determined from the transcript or set to "Next Tuesday at 10:00 AM" if no information is provided.
     - **PARTICIPANTS**: Add all participants from the list.

### CHAIN OF THOUGHTS ###
1. **UNDERSTAND**: Read and analyze the provided inputs (title, participants, transcript).
2. **IDENTIFY**: Extract relevant information for the to-dos and the next meeting.
3. **DIVIDE**: Split the task into two separate processes: creating to-dos and creating the meeting.
4. **STRUCTURE**: Format the results in the required structure for the respective tools.
5. **TRANSMIT**: Pass the data to the designated tools in n8n.
6. **VERIFY**: Ensure the data is correct and complete.

### WHAT YOU SHOULD NOT DO ###
- **NEVER**: Create unclear or vague to-dos.
- **NEVER**: Ignore missing data – use default values where uncertain.
- **NEVER**: Overlook information from the inputs or make incorrect connections.
- **NEVER**: Transmit tasks or meetings without proper formatting.

### OUTPUT EXAMPLES ###
1. **TO-DO**:
   - **TASK DESCRIPTION**: "Prepare presentation for the next meeting."
   - **ASSIGNED PERSON**: "John Doe."
   - **DUE DATE**: "2025-01-25."

2. **MEETING**:
   - **MEETING TITLE**: "Follow-up: Project Discussion."
   - **DATE AND TIME**: "2025-01-28 at 10:00 AM."
   - **PARTICIPANTS**: "John Doe, Jane Example."

### NOTES ###
- EXECUTE YOUR TASKS WITH THE HIGHEST PRECISION AND CONTEXT SENSITIVITY.
- RELY ON THE PROVIDED DATA AND DEFAULT VALUES WHERE NECESSARY.
</system_prompt>

agent: openAiFunctionsAgent
promptType: define
name: Create follow-up call
type: n8n-nodes-base.microsoftOutlookTool
parameters: subject: ={{ $fromAI("meeting_name","Meeting name","string") }}
resource: event
operation: create
calendarId: __rl: yes
mode: list
value: AQMkADAwATNiZmYAZC1jYjE5LWExMzQtMDACLTAwCgBGAAAD1gD8iHcpKEiYQc0w4fCLUgcA-79r8r8ac0aInYGVxRUqCwAAAgEGAAAA-79r8r8ac0aInYGVxRUqCwAAAkH-AAAA
cachedResultName: Calendar
endDateTime: ={{ $fromAI("end_date_time","Date and time of meeting end","string") }}
startDateTime: ={{ $fromAI("start_date_time","Date and time of meeting start","string") }}
descriptionType: manual
toolDescription: =Use tool to create Outlook Calendar Event. Use this tool only when transcript contains information that call should be scheduled.
additionalFields: timeZone: Europe/Berlin
name: Filter: Last 24 hours
type: n8n-nodes-base.filter
parameters: conditions: options: version: 2
caseSensitive: yes
typeValidation: strict
combinator: and
conditions: operator: type: dateTime
operation: afterOrEquals
leftValue: ={{ $json.start_time }}
rightValue: ={{$now.minus({ hours: 24 }).toISO()}}
name: Execute Workflow Trigger
type: n8n-nodes-base.executeWorkflowTrigger
disabled: yes
name: Split Out
type: n8n-nodes-base.splitOut
parameters: fieldToSplitOut: query.items
name: ClickUp
type: n8n-nodes-base.clickUp
parameters: list: 901207046581
name: ={{ $json.name }}
team: 9012366821
space: 90122025710
folder: 90123813376
authentication: oAuth2
additionalFields: content: ={{ $json.description }}
dueDate: ={{ $json.due_date }}
name: Sticky Note
type: n8n-nodes-base.stickyNote
parameters: width: 1000
height: 280
content: ## Sub workflow: Create Task in ClickUp
name: Sticky Note1
type: n8n-nodes-base.stickyNote
parameters: width: 660
height: 520
content: ## Welcome to my Zoom AI Meeting Assistant Workflow!

### This workflow has the following sequence:

1. manual trigger (Can be replaced by a scheduled trigger or a webhook)
2. retrieval of of Zoom meeting data
3. filter the events of the last 24 hours
4. retrieval of transcripts and extract of the text
5. creating a meeting summary, format to html and send per mail
6. create tasks and follow-up call (if discussed in the meeting) in ClickUp/Outlook (can be replaced by Gmail, Airtable, and so forth) via sub workflow

### The following accesses are required for the workflow:
- Zoom Workspace (via API and HTTP Request): [Documentation](https://docs.n8n.io/integrations/builtin/credentials/zoom/)
- Microsoft Outlook: [Documentation](https://docs.n8n.io/integrations/builtin/credentials/microsoft/)
- ClickUp: [Documentation](https://docs.n8n.io/integrations/builtin/credentials/clickup/)
- AI API access (e.g. via OpenAI, Anthropic, Google or Ollama)
- SMTP access data (for sending the mail)

You can contact me via LinkedIn, if you have any questions: https://www.linkedin.com/in/friedemann-schuetz
name: Zoom: Get transcripts data
type: n8n-nodes-base.httpRequest
onError: continueErrorOutput
parameters: url: =https://api.zoom.us/v2/meetings/{{ $json.id }}/recordings
authentication: predefinedCredentialType
nodeCredentialType: zoomOAuth2Api

Connections:
Split Out: main: node: ClickUp
type: main
Create tasks: ai_tool: node: Create tasks and follow-up call
type: ai_tool
Format to html: main: node: Send meeting summary
type: main
OpenAI Chat Model: ai_languageModel: node: Create tasks and follow-up call
type: ai_languageModel
Filter: Only 1 item: main: node: Filter: Only 1 item
type: main
node: Zoom: Get transcript file
type: main
Create follow-up call: ai_tool: node: Create tasks and follow-up call
type: ai_tool
Filter transcript URL: main: node: Filter: Only 1 item
type: main
Filter: Last 24 hours: main: node: Zoom: Get transcripts data
type: main
Create meeting summary: main: node: Sort for mail delivery
type: main
node: Create tasks and follow-up call
type: main
Format transcript text: main: node: Zoom: Get participants data
type: main
Sort for mail delivery: main: node: Format to html
type: main
Execute Workflow Trigger: main: node: Split Out
type: main
Zoom: Get transcript file: main: node: Extract text from transcript file
type: main
Zoom: Get transcripts data: main: node: Filter transcript URL
type: main
node: No Recording/Transcript available
type: main
Zoom: Get participants data: main: node: Create meeting summary
type: main
Zoom: Get data of last meeting: main: node: Filter: Last 24 hours
type: main
Extract text from transcript file: main: node: Format transcript text
type: main
When clicking ‘Test workflow’: main: node: Zoom: Get data of last meeting
type: main