{
  "id": "jhCySM8mocQtyeqh",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Meeting Notes to Action Items Automator",
  "tags": [],
  "nodes": [
    {
      "id": "be31e6e4-a700-49d8-910b-4732919be1e1",
      "name": "Sticky Note - Documentation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        -304
      ],
      "parameters": {
        "width": 820,
        "height": 1536,
        "content": "## Meeting Notes to Action Items Automator\n\nThis AI-powered workflow transcribes Zoom/Google Meet recordings, extracts decisions and tasks using AI, then creates tickets in Jira/ClickUp/Linear and assigns them to team members automatically.\n\n### How it works\n\n1. **Trigger** - Receives meeting recording URL via webhook or schedule\n2. **Download Recording** - Fetches audio/video file from Zoom/Google Meet\n3. **Audio Extraction** - Converts video to audio if needed using FFmpeg\n4. **Transcription** - Uses Whisper API to transcribe meeting audio\n5. **Wait & Process** - Allows transcription to complete\n6. **Parse Transcript** - Cleans and formats the transcription text\n7. **AI Analysis** - Claude extracts action items, decisions, owners\n8. **Team Member Matching** - Maps names to user IDs in project tools\n9. **Create Tasks** - Generates tickets in Jira/ClickUp/Linear\n10. **Assign & Notify** - Assigns tasks to team members and sends notifications\n11. **Meeting Summary** - Saves full summary to Google Drive/Notion\n12. **Response** - Returns processed action items and task links\n\n### Setup Steps\n\n1. Import this workflow into your n8n instance\n2. Configure credentials:\n   - **Zoom OAuth** - For downloading Zoom recordings\n   - **Google OAuth** - For Google Meet recordings and Drive storage\n   - **OpenAI API** - For Whisper transcription service\n   - **Anthropic API** - For Claude AI analysis\n   - **Jira/ClickUp/Linear API** - For task creation\n   - **Slack/Teams** - For notifications (optional)\n3. Set up team member mapping in the config node\n4. Configure your project management tool preferences\n5. Activate the workflow\n\n### Sample Trigger Payload\n```json\n{\n  \"meetingSource\": \"zoom\",\n  \"recordingUrl\": \"https://zoom.us/rec/share/...\",\n  \"meetingTitle\": \"Q1 Planning Meeting\",\n  \"meetingDate\": \"2024-01-15\",\n  \"attendees\": [\"alice@company.com\", \"bob@company.com\", \"charlie@company.com\"],\n  \"projectKey\": \"PROJ-123\",\n  \"taskTool\": \"jira\",\n  \"defaultPriority\": \"medium\",\n  \"autoAssign\": true,\n  \"sendNotifications\": true,\n  \"saveToNotion\": false,\n  \"saveToDrive\": true,\n  \"extractDecisions\": true,\n  \"extractRisks\": true,\n  \"dueDate\": \"2024-01-22\"\n}\n```\n\n### Features\n\n- **Multi-platform support** (Zoom, Google Meet, MS Teams recordings)\n- **Accurate transcription** using OpenAI Whisper API\n- **AI-powered extraction** of action items, decisions, risks, and next steps\n- **Automatic task creation** in Jira, ClickUp, or Linear\n- **Smart assignment** - maps attendee names to task assignees\n- **Meeting summaries** - saves comprehensive notes to Drive/Notion\n- **Slack/Teams notifications** - alerts team members of new tasks\n- **Duplicate detection** - prevents creating duplicate tickets\n- **Priority detection** - AI assigns urgency levels to tasks"
      },
      "typeVersion": 1
    },
    {
      "id": "46817c96-d413-4b88-8081-68569ffe226e",
      "name": "Sticky Note - Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        288
      ],
      "parameters": {
        "color": 5,
        "width": 656,
        "height": 520,
        "content": "## 1. Receive & Download Recording"
      },
      "typeVersion": 1
    },
    {
      "id": "09e53bbc-68aa-4cbd-93da-3f8bd9d0847c",
      "name": "Sticky Note - Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 700,
        "height": 740,
        "content": "## 2. Transcribe & Parse Audio"
      },
      "typeVersion": 1
    },
    {
      "id": "787e1269-ff90-4414-b79a-fdd1b798cf1a",
      "name": "Sticky Note - Section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2144,
        128
      ],
      "parameters": {
        "color": 5,
        "width": 680,
        "height": 660,
        "content": "## 3. AI Analysis & Extraction"
      },
      "typeVersion": 1
    },
    {
      "id": "db570ee0-925a-4527-bdb5-c7ce819f99fa",
      "name": "Sticky Note - Section 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2896,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 2080,
        "height": 800,
        "content": "## 4. Create Tasks & Notify"
      },
      "typeVersion": 1
    },
    {
      "id": "9d970b7a-85f6-47a6-8fc3-c30055cb08e2",
      "name": "Receive Meeting Recording Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        784,
        512
      ],
      "parameters": {
        "path": "meeting-automator",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "0d000a90-4c74-4d7f-a858-5a759f5bee5b",
      "name": "Validate Config & Build Params",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        512
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Extract and validate configuration from request\nconst body = $input.item.json.body || $input.item.json;\n\n// Required fields validation\nif (!body.recordingUrl) {\n  throw new Error('recordingUrl is required');\n}\n\nif (!body.meetingSource || !['zoom', 'google_meet', 'teams'].includes(body.meetingSource)) {\n  throw new Error('meetingSource must be one of: zoom, google_meet, teams');\n}\n\n// Build configuration\nconst config = {\n  meetingSource: body.meetingSource,\n  recordingUrl: body.recordingUrl,\n  meetingTitle: body.meetingTitle || 'Untitled Meeting',\n  meetingDate: body.meetingDate || new Date().toISOString().split('T')[0],\n  attendees: body.attendees || [],\n  projectKey: body.projectKey || 'GENERAL',\n  taskTool: body.taskTool || 'jira', // jira, clickup, linear\n  defaultPriority: body.defaultPriority || 'medium',\n  autoAssign: Boolean(body.autoAssign !== false),\n  sendNotifications: Boolean(body.sendNotifications !== false),\n  saveToNotion: Boolean(body.saveToNotion),\n  saveToDrive: Boolean(body.saveToDrive !== false),\n  extractDecisions: Boolean(body.extractDecisions !== false),\n  extractRisks: Boolean(body.extractRisks !== false),\n  dueDate: body.dueDate || null,\n  transcriptionLanguage: body.transcriptionLanguage || 'en'\n};\n\n// Team member mapping (email to user IDs in different platforms)\nconst teamMapping = body.teamMapping || {\n  // Example structure - should be customized per organization\n  'user@example.com': {\n    name: 'Alice Johnson',\n    jiraAccountId: 'account-id-1',\n    clickupUserId: '12345',\n    linearUserId: 'linear-user-1',\n    slackUserId: 'U01ABC123'\n  },\n  'user@example.com': {\n    name: 'Bob Smith',\n    jiraAccountId: 'account-id-2',\n    clickupUserId: '67890',\n    linearUserId: 'linear-user-2',\n    slackUserId: 'U02DEF456'\n  }\n};\n\n// Metadata\nconst metadata = {\n  workflowId: `MNA-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n  startedAt: new Date().toISOString(),\n  timezone: body.timezone || 'UTC'\n};\n\nreturn {\n  json: {\n    config,\n    teamMapping,\n    metadata,\n    rawBody: body\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "18f3c152-a677-4933-931b-1a4a6402805c",
      "name": "Check Meeting Source",
      "type": "n8n-nodes-base.if",
      "position": [
        1232,
        512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.config.meetingSource }}",
              "rightValue": "zoom"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b8ef3986-0a20-4ce1-8a37-4791770dc8ba",
      "name": "Download Zoom Recording",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1488,
        416
      ],
      "parameters": {
        "url": "={{ $json.config.recordingUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "zoomOAuth2Api"
      },
      "credentials": {
        "zoomOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "83e6c901-25f3-4c1f-8e26-170e51542fe3",
      "name": "Download Google Meet Recording",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1488,
        608
      ],
      "parameters": {
        "url": "={{ $json.config.recordingUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "credentials": {
        "googleOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d540c3b5-b133-4cc6-9d51-305a8f7af6ce",
      "name": "Detect File Type",
      "type": "n8n-nodes-base.code",
      "position": [
        1728,
        512
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Check if file is video (needs audio extraction) or already audio\nconst binaryData = Object.values($input.item.binary)[0];\nconst fileName = binaryData.fileName || '';\nconst mimeType = binaryData.mimeType || '';\n\nconst isVideo = mimeType.includes('video') || fileName.match(/\\.(mp4|mov|avi|mkv)$/i);\nconst isAudio = mimeType.includes('audio') || fileName.match(/\\.(mp3|wav|m4a|ogg)$/i);\n\nreturn {\n  json: {\n    fileName,\n    mimeType,\n    isVideo,\n    isAudio,\n    needsConversion: isVideo,\n    binaryKey: Object.keys($input.item.binary)[0],\n    config: $('Validate Config & Build Params').item.json.config,\n    metadata: $('Validate Config & Build Params').item.json.metadata\n  },\n  binary: $input.item.binary\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "4cf54e0c-0522-484f-bceb-ccf14da45537",
      "name": "Transcribe Audio with Whisper",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1968,
        608
      ],
      "parameters": {
        "options": {},
        "operation": "classify"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6b67881c-2a73-4c34-bd40-ab6962bf5255",
      "name": "Wait for Transcription",
      "type": "n8n-nodes-base.wait",
      "position": [
        2208,
        512
      ],
      "parameters": {
        "resume": "after-execution"
      },
      "typeVersion": 1.1
    },
    {
      "id": "ee566051-4ee7-4a08-98d7-18268d495e8e",
      "name": "Parse & Clean Transcript",
      "type": "n8n-nodes-base.code",
      "position": [
        2448,
        512
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Parse and clean transcription result\nconst transcription = $input.item.json;\nlet transcriptText = transcription.text || transcription.transcription || '';\n\n// Clean up common transcription artifacts\ntranscriptText = transcriptText\n  .replace(/\\[MUSIC\\]/g, '')\n  .replace(/\\[INAUDIBLE\\]/g, '[unclear]')\n  .trim();\n\n// Split into segments if available\nconst segments = transcription.segments || [];\n\n// Get meeting metadata from upstream\nconst config = $('Validate Config & Build Params').item.json.config;\nconst metadata = $('Validate Config & Build Params').item.json.metadata;\n\n// Calculate duration\nconst duration = segments.length > 0 \n  ? segments[segments.length - 1].end \n  : null;\n\n// Count words and speakers\nconst wordCount = transcriptText.split(/\\s+/).length;\nconst speakerCount = new Set(\n  segments.filter(s => s.speaker).map(s => s.speaker)\n).size;\n\nreturn {\n  json: {\n    transcript: {\n      fullText: transcriptText,\n      segments,\n      wordCount,\n      speakerCount,\n      durationSeconds: duration,\n      language: config.transcriptionLanguage\n    },\n    meeting: {\n      title: config.meetingTitle,\n      date: config.meetingDate,\n      attendees: config.attendees,\n      projectKey: config.projectKey\n    },\n    config,\n    metadata\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "5a9a3b2a-2b21-408c-a276-fb41812ab6de",
      "name": "Analyze Transcript with Claude AI",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2592,
        512
      ],
      "parameters": {
        "text": "=You are an expert meeting analyst and project coordinator. Analyze this meeting transcript and extract actionable information.\n\n**Meeting Details:**\n- Title: {{ $json.meeting.title }}\n- Date: {{ $json.meeting.date }}\n- Attendees: {{ $json.meeting.attendees.join(', ') }}\n- Duration: {{ Math.floor($json.transcript.durationSeconds / 60) }} minutes\n- Word Count: {{ $json.transcript.wordCount }}\n\n**Transcript:**\n{{ $json.transcript.fullText }}\n\n**Analysis Requirements:**\n\nExtract and categorize the following in JSON format:\n\n1. **Action Items** - Specific tasks that need to be completed\n   - Task description (clear and actionable)\n   - Assigned person (match to attendee list or extract from context)\n   - Priority (high/medium/low based on urgency discussed)\n   - Due date (if mentioned, otherwise null)\n   - Context (relevant discussion that led to this task)\n\n2. **Decisions Made** - Key decisions and agreements (if extractDecisions: {{ $json.config.extractDecisions }})\n   - Decision description\n   - Decision maker\n   - Impact level (high/medium/low)\n\n3. **Risks Identified** - Potential issues or blockers (if extractRisks: {{ $json.config.extractRisks }})\n   - Risk description\n   - Severity (high/medium/low)\n   - Mentioned by whom\n\n4. **Next Steps** - Follow-up meetings or milestones\n   - Description\n   - Tentative date\n   - Owners\n\n5. **Key Discussion Points** - Main topics covered\n   - Topic\n   - Summary\n   - Time allocation (if determinable)\n\n**Response Format:**\n```json\n{\n  \"actionItems\": [\n    {\n      \"title\": \"Clear, actionable task title\",\n      \"description\": \"Detailed description with context\",\n      \"assignee\": \"person@company.com or name\",\n      \"priority\": \"high\",\n      \"dueDate\": \"2024-01-20\",\n      \"labels\": [\"engineering\", \"urgent\"],\n      \"estimatedHours\": 4,\n      \"dependencies\": [\"Other task if mentioned\"]\n    }\n  ],\n  \"decisions\": [\n    {\n      \"decision\": \"Decision description\",\n      \"decisionMaker\": \"Name\",\n      \"impact\": \"high\",\n      \"rationale\": \"Why this decision was made\"\n    }\n  ],\n  \"risks\": [\n    {\n      \"risk\": \"Risk description\",\n      \"severity\": \"medium\",\n      \"mentionedBy\": \"Name\",\n      \"mitigation\": \"Suggested mitigation if any\"\n    }\n  ],\n  \"nextSteps\": [\n    {\n      \"description\": \"Follow-up action\",\n      \"tentativeDate\": \"2024-01-25\",\n      \"owner\": \"person@company.com\"\n    }\n  ],\n  \"discussionPoints\": [\n    {\n      \"topic\": \"Topic name\",\n      \"summary\": \"Brief summary\",\n      \"timeSpent\": \"5 minutes\"\n    }\n  ],\n  \"meetingSummary\": \"1-2 paragraph executive summary of the entire meeting\",\n  \"attendeesPresent\": [\"List of people who actively participated\"],\n  \"followUpRequired\": true\n}\n```\n\n**Important Guidelines:**\n- Match assignee names/emails to the attendees list when possible\n- If a task doesn't have a clear owner, set assignee to null\n- Be specific in task titles - avoid vague descriptions\n- Extract actual dates mentioned, or use null for due dates\n- Prioritize based on language cues (ASAP, urgent, critical, etc.)\n- Action items should be independently actionable\n- Include labels that help categorize work (engineering, design, marketing, etc.)",
        "options": {
          "systemMessage": "You are an expert meeting analyst who extracts structured, actionable information from meeting transcripts. Always respond in valid JSON format without markdown code blocks. Be precise and thorough."
        },
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "b89c5534-614f-43c6-b260-7938c3c3924c",
      "name": "Claude AI Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        2832,
        752
      ],
      "parameters": {
        "model": "=claude-sonnet-4-20250514",
        "options": {
          "temperature": 0.3
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4dabe6b5-59fb-4323-85e7-f1f63bda7333",
      "name": "Parse Analysis & Match Team Members",
      "type": "n8n-nodes-base.code",
      "position": [
        2928,
        512
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Parse Claude's response\nconst aiResponse = $input.item.json;\nlet aiText = aiResponse.response || aiResponse.output || aiResponse.text || '';\n\n// Handle content array format\nif (aiResponse.content && Array.isArray(aiResponse.content)) {\n  aiText = aiResponse.content[0]?.text || '';\n}\n\n// Clean JSON from markdown\nconst cleanText = aiText\n  .replace(/```json\\s*/g, '')\n  .replace(/```\\s*/g, '')\n  .trim();\n\nlet analysis;\ntry {\n  analysis = JSON.parse(cleanText);\n} catch (error) {\n  throw new Error(`Failed to parse AI analysis: ${error.message}`);\n}\n\n// Get upstream data\nconst config = $('Validate Config & Build Params').item.json.config;\nconst teamMapping = $('Validate Config & Build Params').item.json.teamMapping;\nconst metadata = $('Validate Config & Build Params').item.json.metadata;\nconst transcript = $('Parse & Clean Transcript').item.json.transcript;\nconst meeting = $('Parse & Clean Transcript').item.json.meeting;\n\n// Match assignees to team member IDs\nconst actionItemsWithIds = (analysis.actionItems || []).map((item, index) => {\n  let assigneeData = null;\n  \n  if (item.assignee) {\n    // Try to match by email or name\n    const assigneeLower = item.assignee.toLowerCase();\n    \n    for (const [email, userData] of Object.entries(teamMapping)) {\n      if (\n        email.toLowerCase() === assigneeLower ||\n        userData.name.toLowerCase().includes(assigneeLower) ||\n        assigneeLower.includes(userData.name.toLowerCase().split(' ')[0])\n      ) {\n        assigneeData = {\n          email,\n          ...userData\n        };\n        break;\n      }\n    }\n  }\n  \n  return {\n    ...item,\n    id: `${metadata.workflowId}-task-${index + 1}`,\n    assigneeData,\n    createdFrom: 'meeting-automator'\n  };\n});\n\nreturn {\n  json: {\n    analysis: {\n      ...analysis,\n      actionItems: actionItemsWithIds\n    },\n    meeting,\n    transcript,\n    config,\n    teamMapping,\n    metadata,\n    stats: {\n      totalActionItems: actionItemsWithIds.length,\n      assignedItems: actionItemsWithIds.filter(i => i.assigneeData).length,\n      unassignedItems: actionItemsWithIds.filter(i => !i.assigneeData).length,\n      decisionsCount: (analysis.decisions || []).length,\n      risksCount: (analysis.risks || []).length,\n      highPriorityItems: actionItemsWithIds.filter(i => i.priority === 'high').length\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "d85ab826-8e80-4c31-8734-c64d658b0af6",
      "name": "Route to Task Tool",
      "type": "n8n-nodes-base.if",
      "position": [
        3168,
        512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.config.taskTool }}",
              "rightValue": "jira"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "758fe82d-e50c-442a-9721-da5434a58e27",
      "name": "Create Jira Task",
      "type": "n8n-nodes-base.jira",
      "position": [
        3408,
        416
      ],
      "parameters": {
        "project": {
          "__rl": true,
          "mode": "id",
          "value": "=",
          "__regex": "^([0-9]{2,})"
        },
        "summary": "={{ $item(0).$json.title }}",
        "issueType": "Task",
        "additionalFields": {
          "labels": "={{ $item(0).$json.labels }}",
          "assignee": "={{ $item(0).$json.assigneeData?.jiraAccountId }}",
          "priority": "={{ $item(0).$json.priority.charAt(0).toUpperCase() + $item(0).$json.priority.slice(1) }}"
        }
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "602feaf5-44f9-4f9a-8713-05edc55c1db6",
      "name": "Create ClickUp Task",
      "type": "n8n-nodes-base.clickUp",
      "position": [
        3408,
        608
      ],
      "parameters": {
        "list": "=",
        "name": "={{ $item(0).$json.title }}",
        "team": "=",
        "space": "=",
        "folder": "=",
        "additionalFields": {
          "tags": "={{ $item(0).$json.labels }}",
          "dueDate": "={{ new Date($item(0).$json.dueDate).getTime() }}",
          "priority": "={{ $item(0).$json.priority === 'high' ? 1 : $item(0).$json.priority === 'medium' ? 2 : 3 }}",
          "assignees": "={{ [$item(0).$json.assigneeData?.clickupUserId] }}"
        }
      },
      "credentials": {
        "clickUpApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "178b379e-ff21-4afe-b3d0-4a5ab09f9074",
      "name": "Merge Created Tasks",
      "type": "n8n-nodes-base.merge",
      "position": [
        3648,
        512
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "mergeByFields": {
          "values": [
            {
              "field1": "metadata.workflowId",
              "field2": "metadata.workflowId"
            }
          ]
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "96095213-55d7-4357-bbe3-6358841354da",
      "name": "Send Slack Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        3888,
        416
      ],
      "parameters": {
        "text": "=\ud83c\udfaf New task assigned to you from meeting: **{{ $json.meeting.title }}**\\n\\n**Task:** {{ $item(0).$json.title }}\\n**Priority:** {{ $item(0).$json.priority }}\\n**Due:** {{ $item(0).$json.dueDate || 'No deadline' }}\\n\\n{{ $item(0).$json.description }}\\n\\n\ud83d\udccb View in {{ $json.config.taskTool }}: {{ $item(0).$json.taskUrl }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "=eerr4433rr"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2,
      "continueOnFail": true
    },
    {
      "id": "98304ca3-fa94-47c0-b82c-4c7b1f15e6f2",
      "name": "Save Summary to Google Drive",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        3888,
        608
      ],
      "parameters": {
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": "={{ $json.config.driveFolderId || 'root' }}"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3,
      "continueOnFail": true
    },
    {
      "id": "9002635e-bb09-4e26-8ec6-7d82a87fd5f7",
      "name": "Generate Summary Document",
      "type": "n8n-nodes-base.code",
      "position": [
        3648,
        720
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Build comprehensive meeting summary document\nconst data = $input.item.json;\nconst analysis = data.analysis;\nconst meeting = data.meeting;\nconst stats = data.stats;\n\nconst markdown = `# ${meeting.title}\n**Date:** ${meeting.date}\n**Attendees:** ${meeting.attendees.join(', ')}\n**Duration:** ${Math.floor(data.transcript.durationSeconds / 60)} minutes\n\n---\n\n## Executive Summary\n${analysis.meetingSummary}\n\n## Statistics\n- Total Action Items: ${stats.totalActionItems}\n- High Priority Items: ${stats.highPriorityItems}\n- Decisions Made: ${stats.decisionsCount}\n- Risks Identified: ${stats.risksCount}\n\n---\n\n## Action Items (${stats.totalActionItems})\n\n${analysis.actionItems.map((item, i) => `\n### ${i + 1}. ${item.title}\n- **Assignee:** ${item.assigneeData?.name || item.assignee || 'Unassigned'}\n- **Priority:** ${item.priority.toUpperCase()}\n- **Due Date:** ${item.dueDate || 'Not specified'}\n- **Labels:** ${item.labels?.join(', ') || 'None'}\n- **Estimated Hours:** ${item.estimatedHours || 'TBD'}\n\n${item.description}\n\n**Context:** ${item.context || 'N/A'}\n`).join('\\n')}\n\n---\n\n## Decisions Made (${stats.decisionsCount})\n\n${(analysis.decisions || []).map((dec, i) => `\n${i + 1}. **${dec.decision}**\n   - Decision Maker: ${dec.decisionMaker}\n   - Impact: ${dec.impact.toUpperCase()}\n   - Rationale: ${dec.rationale}\n`).join('\\n')}\n\n---\n\n## Risks & Blockers (${stats.risksCount})\n\n${(analysis.risks || []).map((risk, i) => `\n${i + 1}. **${risk.risk}**\n   - Severity: ${risk.severity.toUpperCase()}\n   - Mentioned By: ${risk.mentionedBy}\n   - Mitigation: ${risk.mitigation || 'TBD'}\n`).join('\\n')}\n\n---\n\n## Key Discussion Points\n\n${(analysis.discussionPoints || []).map((point, i) => `\n### ${point.topic}\n${point.summary}\n*Time spent: ${point.timeSpent}*\n`).join('\\n')}\n\n---\n\n## Next Steps\n\n${(analysis.nextSteps || []).map((step, i) => `\n${i + 1}. ${step.description}\n   - Owner: ${step.owner}\n   - Tentative Date: ${step.tentativeDate || 'TBD'}\n`).join('\\n')}\n\n---\n\n## Full Transcript\n\n${data.transcript.fullText}\n\n---\n\n*Generated by Meeting Notes Automator*\n*Workflow ID: ${data.metadata.workflowId}*\n*Generated at: ${new Date().toISOString()}*\n`;\n\nreturn {\n  json: {\n    ...data,\n    summaryDocument: markdown\n  },\n  binary: {\n    data: {\n      data: Buffer.from(markdown).toString('base64'),\n      mimeType: 'text/markdown',\n      fileName: `${meeting.title} - Meeting Notes - ${meeting.date}.md`\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "ed19032d-1269-4346-a64b-afb0ef9c9b45",
      "name": "Format Final Response",
      "type": "n8n-nodes-base.code",
      "position": [
        4128,
        512
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Build final response with all task links and summary\nconst allData = $input.all();\nconst mainData = allData[0].json;\nconst createdTasks = allData.slice(1).map(item => item.json);\n\nconst response = {\n  success: true,\n  message: `Successfully processed meeting and created ${mainData.stats.totalActionItems} action items`,\n  meeting: {\n    title: mainData.meeting.title,\n    date: mainData.meeting.date,\n    attendees: mainData.meeting.attendees,\n    duration: `${Math.floor(mainData.transcript.durationSeconds / 60)} minutes`,\n    wordCount: mainData.transcript.wordCount\n  },\n  summary: {\n    executiveSummary: mainData.analysis.meetingSummary,\n    totalActionItems: mainData.stats.totalActionItems,\n    assignedItems: mainData.stats.assignedItems,\n    unassignedItems: mainData.stats.unassignedItems,\n    highPriorityItems: mainData.stats.highPriorityItems,\n    decisionsCount: mainData.stats.decisionsCount,\n    risksCount: mainData.stats.risksCount\n  },\n  actionItems: mainData.analysis.actionItems.map(item => ({\n    id: item.id,\n    title: item.title,\n    assignee: item.assigneeData?.name || item.assignee || 'Unassigned',\n    priority: item.priority,\n    dueDate: item.dueDate,\n    taskUrl: createdTasks.find(t => t.summary === item.title)?.url || '#',\n    status: 'created'\n  })),\n  decisions: mainData.analysis.decisions || [],\n  risks: mainData.analysis.risks || [],\n  nextSteps: mainData.analysis.nextSteps || [],\n  notifications: {\n    sent: mainData.config.sendNotifications,\n    recipientCount: mainData.stats.assignedItems\n  },\n  storage: {\n    savedToDrive: mainData.config.saveToDrive,\n    savedToNotion: mainData.config.saveToNotion,\n    summaryUrl: '#' // Would be populated by Drive/Notion response\n  },\n  metadata: {\n    workflowId: mainData.metadata.workflowId,\n    processedAt: new Date().toISOString(),\n    taskTool: mainData.config.taskTool\n  }\n};\n\nreturn [{ json: response }];"
      },
      "typeVersion": 2
    },
    {
      "id": "55ec249f-3f23-4ffe-93a2-5f020361741b",
      "name": "Send Response to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4576,
        512
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json, null, 2) }}"
      },
      "typeVersion": 1
    },
    {
      "id": "d3589eed-ecac-4bc4-8a4e-48f332f27044",
      "name": "Audio Extraction",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1952,
        288
      ],
      "parameters": {
        "url": "={{ $json.config.Audio }}",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "3edfba60-6b3d-4276-b7c5-f192ab7a3224",
      "name": "Wait For Response Send",
      "type": "n8n-nodes-base.wait",
      "position": [
        4336,
        512
      ],
      "parameters": {},
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "c35b7d37-df8a-41e3-b873-9948fcb0cd91",
  "connections": {
    "Claude AI Model": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze Transcript with Claude AI",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Audio Extraction": {
      "main": [
        [
          {
            "node": "Wait for Transcription",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Jira Task": {
      "main": [
        [
          {
            "node": "Merge Created Tasks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect File Type": {
      "main": [
        [
          {
            "node": "Transcribe Audio with Whisper",
            "type": "main",
            "index": 0
          },
          {
            "node": "Audio Extraction",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Task Tool": {
      "main": [
        [
          {
            "node": "Create Jira Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create ClickUp Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create ClickUp Task": {
      "main": [
        [
          {
            "node": "Merge Created Tasks",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Created Tasks": {
      "main": [
        [
          {
            "node": "Generate Summary Document",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Slack Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Meeting Source": {
      "main": [
        [
          {
            "node": "Download Zoom Recording",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download Google Meet Recording",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Final Response": {
      "main": [
        [
          {
            "node": "Wait For Response Send",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait For Response Send": {
      "main": [
        [
          {
            "node": "Send Response to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Transcription": {
      "main": [
        [
          {
            "node": "Parse & Clean Transcript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Zoom Recording": {
      "main": [
        [
          {
            "node": "Detect File Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Slack Notification": {
      "main": [
        [
          {
            "node": "Format Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Clean Transcript": {
      "main": [
        [
          {
            "node": "Analyze Transcript with Claude AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Summary Document": {
      "main": [
        [
          {
            "node": "Save Summary to Google Drive",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Summary to Google Drive": {
      "main": [
        [
          {
            "node": "Format Final Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe Audio with Whisper": {
      "main": [
        [
          {
            "node": "Wait for Transcription",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Google Meet Recording": {
      "main": [
        [
          {
            "node": "Detect File Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Config & Build Params": {
      "main": [
        [
          {
            "node": "Check Meeting Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Transcript with Claude AI": {
      "main": [
        [
          {
            "node": "Parse Analysis & Match Team Members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Meeting Recording Webhook": {
      "main": [
        [
          {
            "node": "Validate Config & Build Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Analysis & Match Team Members": {
      "main": [
        [
          {
            "node": "Route to Task Tool",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}