{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "7d75ad19-39d1-4348-ac42-9a29dd83366e",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -544,
        288
      ],
      "parameters": {
        "color": 4,
        "width": 492,
        "height": 1020,
        "content": "## Meeting Auto-Categorizer \u2014 Fireflies + GPT-4o-mini + Google Sheets\n\nFor teams who want every meeting automatically tagged with a category the moment it ends. When Fireflies finishes transcribing a meeting, this workflow fetches the transcript summary, sends it to GPT-4o-mini for classification, and logs one row to Google Sheets with the category, confidence level, and a one-sentence reason. After a few weeks you can filter the sheet by Category to see all Sales calls, all Internal syncs, and so on.\n\n## How it works\n- **1. Webhook \u2014 Fireflies Transcript Done** receives the meetingId from Fireflies on transcription complete\n- **2. Set \u2014 Config Values** stores your Fireflies API key, Google Sheet ID, sheet tab name, and extracts the meetingId from the webhook payload\n- **3. HTTP \u2014 Fetch Transcript** calls the Fireflies GraphQL API for title, date, duration, participants, transcript URL, keywords, overview, and action items\n- **4. Code \u2014 Extract Meeting Data** cleans and structures the response \u2014 throws an error if the transcript is not found so the execution stops cleanly\n- **5. AI Agent \u2014 Categorize Meeting** uses GPT-4o-mini (temperature 0.1) to assign exactly one of seven categories: Sales, Client, Internal, HR, Product, Finance, or Other\n- **8. Code \u2014 Prepare Sheet Row** adds category and confidence emojis for visual scanning in the sheet\n- **9. Google Sheets \u2014 Log Meeting Category** appends one row per meeting automatically\n\n## Set up steps\n1. Activate the workflow and copy the webhook URL from node 1\n2. In Fireflies \u2014 go to Settings, Developer Settings, Webhooks, and paste the URL\n3. In **2. Set \u2014 Config Values** \u2014 replace YOUR_FIREFLIES_API_KEY and YOUR_GOOGLE_SHEET_ID\n4. In **6. OpenAI \u2014 GPT-4o-mini Model** \u2014 connect your OpenAI credential\n5. In **9. Google Sheets \u2014 Log Meeting Category** \u2014 connect your Google Sheets OAuth2 credential\n6. Create a Google Sheet tab named Meeting Categories with columns: Date, Meeting Title, Category, Confidence, Reason, Duration (min), Participants, Keywords, Fireflies URL, Logged At"
      },
      "typeVersion": 1
    },
    {
      "id": "e18a2a2d-22dc-473c-aad3-ba19696988fa",
      "name": "Section \u2014 Webhook, Config, and Transcript Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        544
      ],
      "parameters": {
        "color": 5,
        "width": 660,
        "height": 372,
        "content": "## Webhook, Config, and Transcript Fetch\nFireflies POSTs a meetingId here on transcription complete. Config stores credentials and extracts the meetingId inline. HTTP fetches the lightweight transcript summary \u2014 no full sentences needed."
      },
      "typeVersion": 1
    },
    {
      "id": "6d6fd196-e766-4171-8916-21ec55a61710",
      "name": "Section \u2014 Meeting Data Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        448
      ],
      "parameters": {
        "color": 6,
        "width": 280,
        "height": 564,
        "content": "## Meeting Data Extraction\nCleans and structures the Fireflies response. Extracts title, date, duration, participants, keywords, overview, and action items. Throws an error if no transcript is found \u2014 execution stops cleanly without an IF node."
      },
      "typeVersion": 1
    },
    {
      "id": "1fa087a9-5f08-4bcc-937d-270904e2f397",
      "name": "Section \u2014 AI Meeting Categorization",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1072,
        384
      ],
      "parameters": {
        "color": 6,
        "width": 340,
        "height": 868,
        "content": "## AI Meeting Categorization\nGPT-4o-mini (temperature 0.1) assigns exactly one of seven categories: Sales, Client, Internal, HR, Product, Finance, or Other. Returns category, confidence level (High/Medium/Low), and a one-sentence reason. Structured Output Parser enforces the schema."
      },
      "typeVersion": 1
    },
    {
      "id": "cb040d24-9f5d-4b75-b6f3-8cc3c880fb32",
      "name": "Section \u2014 Row Assembly and Sheet Logging",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        544
      ],
      "parameters": {
        "color": 4,
        "width": 468,
        "height": 372,
        "content": "## Row Assembly and Sheet Logging\nAdds category and confidence emojis for visual scanning. Appends one row per meeting to Google Sheets automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "cd884f90-f451-4e80-bff2-06cfce2f8dc8",
      "name": "1. Webhook \u2014 Fireflies Transcript Done",
      "type": "n8n-nodes-base.webhook",
      "position": [
        64,
        688
      ],
      "parameters": {
        "path": "fireflies-categorize",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6d4c7c1b-2743-4faa-bb86-37b6df39977a",
      "name": "2. Set \u2014 Config Values",
      "type": "n8n-nodes-base.set",
      "position": [
        288,
        688
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-001",
              "name": "firefliesApiKey",
              "type": "string",
              "value": "YOUR_FIREFLIES_API_KEY"
            },
            {
              "id": "cfg-002",
              "name": "sheetId",
              "type": "string",
              "value": "YOUR_GOOGLE_SHEET_ID"
            },
            {
              "id": "cfg-003",
              "name": "sheetName",
              "type": "string",
              "value": "Meeting Categories"
            },
            {
              "id": "cfg-004",
              "name": "meetingId",
              "type": "string",
              "value": "={{ $json.meetingId || $json.body?.meetingId || $json.data?.meetingId || '' }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2e987360-d714-4d52-b604-b1b8cd975951",
      "name": "3. HTTP \u2014 Fetch Transcript",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        512,
        688
      ],
      "parameters": {
        "url": "https://api.fireflies.ai/graphql",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"query\": \"query GetTranscript($id: String!) { transcript(id: $id) { id title date duration participants transcript_url summary { keywords overview action_items } } }\",\n  \"variables\": {\n    \"id\": \"{{ $json.meetingId }}\"\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Authorization",
              "value": "=Bearer {{ $json.firefliesApiKey }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6a8fc13c-9956-4963-97c5-f39cbc89ed08",
      "name": "4. Code \u2014 Extract Meeting Data",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        688
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst config = $('2. Set \u2014 Config Values').item.json;\n\nconst t = response?.data?.transcript;\nif (!t || !t.id) throw new Error('Transcript not found for meetingId: ' + config.meetingId);\n\nconst summary = t.summary || {};\nconst keywords = (summary.keywords || []).slice(0, 12).join(', ') || 'None';\nconst overview = (summary.overview || '').substring(0, 600);\nconst actionItems = (summary.action_items || []).slice(0, 5).join(' | ') || 'None';\n\nconst meetingDate = t.date\n  ? new Date(t.date).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })\n  : new Date().toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });\n\nreturn [{\n  json: {\n    meetingId: t.id,\n    title: t.title || 'Untitled Meeting',\n    meetingDate,\n    durationMin: Math.round((t.duration || 0) / 60),\n    participants: (t.participants || []).join(', ') || 'Unknown',\n    transcriptUrl: t.transcript_url || 'Not available',\n    keywords,\n    overview,\n    actionItems,\n    loggedAt: new Date().toISOString().replace('T', ' ').substring(0, 16),\n    sheetId: config.sheetId,\n    sheetName: config.sheetName\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4068dbe3-210a-45ef-a396-c6a81bca9e15",
      "name": "5. AI Agent \u2014 Categorize Meeting",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1120,
        688
      ],
      "parameters": {
        "text": "=You are a meeting categorization assistant.\n\nAnalyze this meeting and assign it exactly ONE category.\n\nMEETING TITLE: {{ $json.title }}\nKEYWORDS: {{ $json.keywords }}\nOVERVIEW: {{ $json.overview }}\nACTION ITEMS: {{ $json.actionItems }}\nPARTICIPANTS: {{ $json.participants }}\nDURATION: {{ $json.durationMin }} minutes\n\nChoose EXACTLY ONE category from this list:\n- Sales: demos, proposals, pricing discussions, lead calls, prospect outreach, deal negotiations\n- Client: existing client calls, account reviews, onboarding, support calls, client updates\n- Internal: team syncs, standups, planning sessions, retrospectives, internal project discussions\n- HR: interviews, performance reviews, hiring discussions, onboarding new staff, HR policy\n- Product: product roadmap, feature discussions, design reviews, engineering, tech discussions\n- Finance: budget reviews, invoicing, cost discussions, financial planning, accounting\n- Other: does not clearly fit any above category\n\nReturn ONLY a valid JSON object with exactly 3 fields. No extra text. No markdown. No backticks.\n\ncategory \u2014 exactly one of: Sales, Client, Internal, HR, Product, Finance, Other\n\nconfidence \u2014 exactly one of: High, Medium, Low\nHigh = title and keywords clearly match the category\nMedium = some signals match but not definitive\nLow = limited information, best guess\n\nreason \u2014 one plain sentence (under 20 words) explaining why this category was chosen.",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "a3de9318-ded9-4554-9388-e455527868b0",
      "name": "6. OpenAI \u2014 GPT-4o-mini Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1120,
        944
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "maxTokens": 150,
          "temperature": 0.1
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "0d378c8c-d714-421c-8b50-2cbfe8566276",
      "name": "7. Parser \u2014 Structured Category Output",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1264,
        1104
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"category\": {\n      \"type\": \"string\",\n      \"description\": \"Exactly one of: Sales, Client, Internal, HR, Product, Finance, Other\"\n    },\n    \"confidence\": {\n      \"type\": \"string\",\n      \"description\": \"Exactly one of: High, Medium, Low\"\n    },\n    \"reason\": {\n      \"type\": \"string\",\n      \"description\": \"One sentence under 20 words explaining the category choice\"\n    }\n  },\n  \"required\": [\"category\", \"confidence\", \"reason\"]\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "d6095d15-3552-4c80-befc-b61a46a531aa",
      "name": "8. Code \u2014 Prepare Sheet Row",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        688
      ],
      "parameters": {
        "jsCode": "const aiOutput = $input.first().json.output;\nconst meetingData = $('4. Code \u2014 Extract Meeting Data').item.json;\n\nconst category = aiOutput?.category || 'Other';\nconst confidence = aiOutput?.confidence || 'Low';\nconst reason = aiOutput?.reason || 'Could not determine category';\n\nconst categoryEmoji = {\n  'Sales': '\ud83d\udcb0',\n  'Client': '\ud83e\udd1d',\n  'Internal': '\ud83c\udfe2',\n  'HR': '\ud83d\udc65',\n  'Product': '\ud83d\udee0\ufe0f',\n  'Finance': '\ud83d\udcca',\n  'Other': '\ud83d\udccb'\n};\n\nconst confidenceEmoji = {\n  'High': '\u2705',\n  'Medium': '\ud83d\udfe1',\n  'Low': '\u26a0\ufe0f'\n};\n\nreturn [{\n  json: {\n    meetingDate: meetingData.meetingDate,\n    title: meetingData.title,\n    category: (categoryEmoji[category] || '\ud83d\udccb') + ' ' + category,\n    confidence: (confidenceEmoji[confidence] || '\ud83d\udfe1') + ' ' + confidence,\n    reason,\n    durationMin: meetingData.durationMin,\n    participants: meetingData.participants,\n    keywords: meetingData.keywords,\n    transcriptUrl: meetingData.transcriptUrl,\n    loggedAt: meetingData.loggedAt,\n    sheetId: meetingData.sheetId,\n    sheetName: meetingData.sheetName\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1f25c997-1633-43dc-8f1a-a455e8c26799",
      "name": "9. Google Sheets \u2014 Log Meeting Category",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1808,
        688
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.meetingDate }}",
            "Reason": "={{ $json.reason }}",
            "Category": "={{ $json.category }}",
            "Keywords": "={{ $json.keywords }}",
            "Logged At": "={{ $json.loggedAt }}",
            "Confidence": "={{ $json.confidence }}",
            "Participants": "={{ $json.participants }}",
            "Fireflies URL": "={{ $json.transcriptUrl }}",
            "Meeting Title": "={{ $json.title }}",
            "Duration (min)": "={{ $json.durationMin }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {
          "cellFormat": "USER_ENTERED"
        },
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.sheetName }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.sheetId }}"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "connections": {
    "2. Set \u2014 Config Values": {
      "main": [
        [
          {
            "node": "3. HTTP \u2014 Fetch Transcript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. HTTP \u2014 Fetch Transcript": {
      "main": [
        [
          {
            "node": "4. Code \u2014 Extract Meeting Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8. Code \u2014 Prepare Sheet Row": {
      "main": [
        [
          {
            "node": "9. Google Sheets \u2014 Log Meeting Category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. OpenAI \u2014 GPT-4o-mini Model": {
      "ai_languageModel": [
        [
          {
            "node": "5. AI Agent \u2014 Categorize Meeting",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "4. Code \u2014 Extract Meeting Data": {
      "main": [
        [
          {
            "node": "5. AI Agent \u2014 Categorize Meeting",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. AI Agent \u2014 Categorize Meeting": {
      "main": [
        [
          {
            "node": "8. Code \u2014 Prepare Sheet Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Webhook \u2014 Fireflies Transcript Done": {
      "main": [
        [
          {
            "node": "2. Set \u2014 Config Values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Parser \u2014 Structured Category Output": {
      "ai_outputParser": [
        [
          {
            "node": "5. AI Agent \u2014 Categorize Meeting",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    }
  }
}