{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "5cd67a50-9bbf-45d7-9fbe-1e488ba9de38",
      "name": "Step 2 \u2014 Groq Whisper Info",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1648,
        688
      ],
      "parameters": {
        "color": 6,
        "width": 360,
        "height": 292,
        "content": "**\ud83c\udf99\ufe0f STEP 2 \u2014 GROQ SETUP (Audio Transcription)**\n\nFree tier: 28,800 seconds/day \u2014 no card needed!\n\n1. Go to console.groq.com \u2192 API Keys \u2192 Create Key\n2. In n8n \u2192 Settings \u2192 Credentials \u2192 New \u2192 **Header Auth**\n   - Name: `Authorization`\n   - Value: `Bearer YOUR_GROQ_KEY_HERE`\n3. In the **Transcribe Audio** node \u2192 select this credential\n\nSupported formats: .mp3 .mp4 .wav .m4a .webm\nModel: whisper-large-v3-turbo (fast + accurate)"
      },
      "typeVersion": 1
    },
    {
      "id": "e79e4026-e39a-40f6-b295-3cb156de4d12",
      "name": "Step 3 \u2014 OpenRouter Info",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        688
      ],
      "parameters": {
        "color": 5,
        "width": 360,
        "height": 256,
        "content": "**\ud83e\udd16 STEP 3 \u2014 OPENROUTER SETUP (AI Summarization)**\n\n1. Go to openrouter.ai \u2192 Keys \u2192 Create Key\n2. In n8n \u2192 Settings \u2192 Credentials \u2192 New \u2192 **Header Auth**\n   - Name: `Authorization`\n   - Value: `Bearer YOUR_OPENROUTER_KEY_HERE`\n3. In the **Call AI API** node \u2192 select this credential\n\nFree models available!\nRecommended: openai/gpt-4o-mini"
      },
      "typeVersion": 1
    },
    {
      "id": "6d1a11b5-fcf0-4b2a-9528-7d97e1fbe9e2",
      "name": "Step 4 \u2014 Google Docs Info",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2656,
        688
      ],
      "parameters": {
        "color": 5,
        "width": 360,
        "height": 252,
        "content": "**\ud83d\udcc4 STEP 4 \u2014 GOOGLE DOCS SETUP**\n\n1. In n8n \u2192 Settings \u2192 Credentials \u2192 New \u2192 Google Docs OAuth2\n2. Sign in with your Google account and allow access\n3. Select this credential in both Google Docs nodes\n\nThe summary doc will be created in your Google Drive root folder. You can move or rename it afterwards."
      },
      "typeVersion": 1
    },
    {
      "id": "99cba79f-09fe-4776-8e8d-8216d5d242de",
      "name": "Transcribe Audio",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1664,
        1248
      ],
      "parameters": {
        "url": "https://api.groq.com/openai/v1/audio/transcriptions",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "audio"
            },
            {
              "name": "model",
              "value": "whisper-large-v3-turbo"
            },
            {
              "name": "response_format",
              "value": "json"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ce687da7-b142-4095-9fa2-621d7d2faee6",
      "name": "Prepare Audio Input",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        1248
      ],
      "parameters": {
        "jsCode": "const transcription = $input.first().json.text;\n\nif (!transcription || transcription.trim() === '') {\n  throw new Error('Audio transcription failed or returned empty. Check your audio file format (.mp3, .mp4, .wav, .m4a)');\n}\n\nconst webhookData = $('Receive Meeting Transcript').first().json;\nconst body = webhookData.body || {};\n\nconst meetingTitle = body.meeting_title || 'Untitled Meeting';\nconst meetingDate = body.meeting_date || new Date().toISOString().split('T')[0];\n\nreturn [{\n  json: {\n    transcript: transcription.trim().replaceAll('\\n', ' '),\n    meeting_title: meetingTitle,\n    meeting_date: meetingDate\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "60cbf23c-119e-4ddc-8262-9ace39ed5f6c",
      "name": "Call AI API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2160,
        1120
      ],
      "parameters": {
        "url": "https://openrouter.ai/api/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"model\": \"openai/gpt-4o-mini\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You are an expert meeting analyst. Read the meeting transcript and return a structured JSON with exactly these 4 keys:\\n- summary: string (2-3 sentence high-level overview)\\n- decisions: array of strings (each decision made)\\n- action_items: array of objects with keys: task (string), owner (string or TBD), deadline (string or Not specified)\\n- next_agenda: array of strings (suggested topics for next meeting)\\n\\nRespond with ONLY the raw JSON object. No markdown, no code fences, no extra text.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"Meeting Title: {{ $json.meeting_title }}\\nMeeting Date: {{ $json.meeting_date }}\\n\\nTranscript:\\n{{ $json.transcript }}\"\n    }\n  ]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Title",
              "value": "Meeting Summary Workflow"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8a0c9a71-d42b-41ff-bac5-297fc1eba1ff",
      "name": "Step 1 \u2014 Trigger Info1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1216,
        688
      ],
      "parameters": {
        "color": 5,
        "width": 344,
        "height": 380,
        "content": "**\u2699\ufe0f STEP 1 \u2014 TRIGGER SETUP**\n\n**\ud83d\udcdd Text Input:**\nSend POST with JSON body:\n```json\n{\n  \"transcript\": \"Full meeting transcript...\",\n  \"meeting_title\": \"Weekly Sync\",\n  \"meeting_date\": \"2025-05-05\"\n}\n```\n\n**\ud83c\udf99\ufe0f Audio Input:**\nSend POST with multipart/form-data:\n- `audio` \u2192 your audio file\n- `meeting_title` \u2192 optional\n- `meeting_date` \u2192 optional\n\nWorkflow auto-detects which type was sent!"
      },
      "typeVersion": 1
    },
    {
      "id": "f708e8ea-ce08-491c-bcff-60e529eadf57",
      "name": "Receive Meeting Transcript",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1216,
        1120
      ],
      "parameters": {
        "path": "meeting-summary",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "e704c843-cf60-401e-8f26-0f5bcb6860f1",
      "name": "Text or Audio?",
      "type": "n8n-nodes-base.if",
      "position": [
        1408,
        1120
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "606f2461-5dd2-4dc5-9235-0eab499d74e3",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.body.transcript }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "425873e9-392c-4439-94d3-c7e74d508133",
      "name": "Validate & Prepare Input",
      "type": "n8n-nodes-base.code",
      "position": [
        1760,
        1008
      ],
      "parameters": {
        "jsCode": "const body = $input.first().json.body;\n\nif (!body || !body.transcript || body.transcript.trim() === '') {\n  throw new Error('Missing required field: transcript. Please send your meeting transcript in the POST body as JSON.');\n}\n\nconst meetingTitle = body.meeting_title || 'Untitled Meeting';\nconst meetingDate = body.meeting_date || new Date().toISOString().split('T')[0];\nconst transcript = body.transcript.trim().replaceAll('\\n', ' ');\n\nreturn [{\n  json: {\n    transcript,\n    meeting_title: meetingTitle,\n    meeting_date: meetingDate\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "be558b97-9bf7-4760-bdf0-e571ff2e0b5f",
      "name": "Format Document Content",
      "type": "n8n-nodes-base.code",
      "position": [
        2384,
        1120
      ],
      "parameters": {
        "jsCode": "const rawContent = $input.first().json.choices[0].message.content;\n\nlet parsed;\ntry {\n  const clean = rawContent.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch (e) {\n  throw new Error('AI returned invalid JSON. Raw response: ' + rawContent);\n}\n\n// Get meeting meta \u2014 works for both text and audio paths\nlet meetingTitle = 'Untitled Meeting';\nlet meetingDate = new Date().toISOString().split('T')[0];\n\ntry {\n  const textData = $('Validate & Prepare Input').first().json;\n  meetingTitle = textData.meeting_title || meetingTitle;\n  meetingDate = textData.meeting_date || meetingDate;\n} catch (e) {\n  try {\n    const audioData = $('Prepare Audio Input').first().json;\n    meetingTitle = audioData.meeting_title || meetingTitle;\n    meetingDate = audioData.meeting_date || meetingDate;\n  } catch (e2) {\n    // use defaults set above\n  }\n}\n\nconst docTitle = `Meeting Summary \u2014 ${meetingTitle} \u2014 ${meetingDate}`;\n\n// Format Action Items\nlet actionItemsText = '';\nif (Array.isArray(parsed.action_items) && parsed.action_items.length > 0) {\n  parsed.action_items.forEach((item, i) => {\n    actionItemsText += `${i + 1}. ${item.task}\\n   Owner: ${item.owner || 'TBD'}\\n   Deadline: ${item.deadline || 'Not specified'}\\n\\n`;\n  });\n} else {\n  actionItemsText = 'No action items identified.\\n';\n}\n\n// Format Decisions\nlet decisionsText = '';\nif (Array.isArray(parsed.decisions) && parsed.decisions.length > 0) {\n  parsed.decisions.forEach(d => { decisionsText += `\u2022 ${d}\\n`; });\n} else {\n  decisionsText = 'No explicit decisions recorded.\\n';\n}\n\n// Format Next Agenda\nlet agendaText = '';\nif (Array.isArray(parsed.next_agenda) && parsed.next_agenda.length > 0) {\n  parsed.next_agenda.forEach((a, i) => { agendaText += `${i + 1}. ${a}\\n`; });\n} else {\n  agendaText = 'No agenda suggestions.\\n';\n}\n\nconst docContent = `MEETING SUMMARY\\n${'='.repeat(50)}\\nMeeting: ${meetingTitle}\\nDate: ${meetingDate}\\nGenerated: ${new Date().toLocaleString()}\\n\\n${'\u2500'.repeat(50)}\\n\\n\ud83d\udccc EXECUTIVE SUMMARY\\n${'\u2500'.repeat(50)}\\n${parsed.summary || 'No summary generated.'}\\n\\n${'\u2500'.repeat(50)}\\n\\n\u2705 DECISIONS MADE\\n${'\u2500'.repeat(50)}\\n${decisionsText}\\n${'\u2500'.repeat(50)}\\n\\n\ud83d\udccb ACTION ITEMS\\n${'\u2500'.repeat(50)}\\n${actionItemsText}${'\u2500'.repeat(50)}\\n\\n\ud83d\uddd3\ufe0f NEXT MEETING AGENDA\\n${'\u2500'.repeat(50)}\\n${agendaText}\\n${'\u2500'.repeat(50)}\\nAuto-generated by n8n | Meeting Summary Workflow`;\n\nreturn [{\n  json: {\n    doc_title: docTitle,\n    doc_content: docContent,\n    meeting_title: meetingTitle,\n    meeting_date: meetingDate,\n    summary: parsed.summary,\n    decisions: parsed.decisions,\n    action_items: parsed.action_items,\n    next_agenda: parsed.next_agenda,\n    action_items_count: Array.isArray(parsed.action_items) ? parsed.action_items.length : 0\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "15812d76-6bb0-438f-9905-66f8c1ba9b33",
      "name": "Write Summary to Doc",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        2816,
        1120
      ],
      "parameters": {
        "actionsUi": {
          "actionFields": [
            {
              "text": "={{ $('Format Document Content').first().json.doc_content }}",
              "action": "insert"
            }
          ]
        },
        "operation": "update",
        "documentURL": "={{ $json.id }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b5d098de-7750-42da-bc2f-ef57a92f9a53",
      "name": "Create Google Doc",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        2608,
        1120
      ],
      "parameters": {
        "title": "={{ $json.doc_title }}",
        "folderId": "1XC8faFgUSySqlfST6tjXVAPJDSaCQxUq"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8683ee43-7d40-41c3-b828-93340906c259",
      "name": "Return Doc Link",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3040,
        1120
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({\n  success: true,\n  message: 'Meeting summary created successfully',\n  doc_title: $('Format Document Content').first().json.doc_title,\n  google_doc_id: $('Create Google Doc').first().json.id,\n  google_doc_url: 'https://docs.google.com/document/d/' + $('Create Google Doc').first().json.id + '/edit',\n  summary_preview: $('Format Document Content').first().json.summary,\n  action_items_count: $('Format Document Content').first().json.action_items_count\n}) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7aba0f63-e450-4238-8eca-b291b1190317",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        688
      ],
      "parameters": {
        "width": 624,
        "height": 560,
        "content": "## \ud83d\udccb Meeting Summary \u2192 Action Items \n\n**Who is this for?**\nAnyone who wants to turn raw meeting transcripts OR audio recordings into structured, ready-to-use summaries \u2014 without manually writing notes.\n\n**What it does:**\nSend transcript text OR upload an audio file \u2192 AI generates a clean Google Doc with:\n- \u2705 Executive Summary (2\u20133 lines)\n- \u2705 Key Decisions Made\n- \u2705 Action Items (numbered, with Owner & Deadline)\n- \u2705 Suggested Next Meeting Agenda\n\n**Supports Two Input Types:**\n\ud83d\udcdd Text \u2014 Paste transcript as JSON\n\ud83c\udf99\ufe0f Audio \u2014 Upload .mp3 / .mp4 / .wav / .m4a (auto-transcribed via Groq Whisper)\n\n**How it works:**\n1. Send POST request with text OR audio file\n2. Workflow auto-detects input type\n3. Audio is transcribed via Groq Whisper (free, 28,800 sec/day)\n4. AI structures content into 4 sections\n5. Google Doc is created and link returned\n\n**Requirements:**\n- Groq API Key \u2192 stored as Header Auth credential (free)\n- OpenRouter API Key \u2192 stored as Header Auth credential\n- Google Docs OAuth2 credentials"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Call AI API": {
      "main": [
        [
          {
            "node": "Format Document Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text or Audio?": {
      "main": [
        [
          {
            "node": "Validate & Prepare Input",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Transcribe Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe Audio": {
      "main": [
        [
          {
            "node": "Prepare Audio Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Google Doc": {
      "main": [
        [
          {
            "node": "Write Summary to Doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Audio Input": {
      "main": [
        [
          {
            "node": "Call AI API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Summary to Doc": {
      "main": [
        [
          {
            "node": "Return Doc Link",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Document Content": {
      "main": [
        [
          {
            "node": "Create Google Doc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Prepare Input": {
      "main": [
        [
          {
            "node": "Call AI API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Meeting Transcript": {
      "main": [
        [
          {
            "node": "Text or Audio?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}