{
  "name": "📋 Meeting Task Routing (Template)",
  "nodes": [
    {
      "parameters": {
        "content": "# 📋 Meeting Task Routing\n\nAI meeting notes hit a Google Drive folder -> AI pulls out the tasks -> each person gets their to-do list on Slack, linked to the notes.\n\n**Set before running:** (1) Drive folder  (2) AI key  (3) team→Slack-ID table  (4) fallback Slack IDs  (5) Slack connection.\nReplace every `PASTE_...`, `YOUR_...`, `SLACK_..._ID`.",
        "height": 170,
        "width": 900,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -720,
        -560
      ],
      "id": "sticky-README",
      "name": "README"
    },
    {
      "parameters": {
        "content": "## (1) Watch & fetch\nChecks the folder every minute -> waits 5 min for notes to finish -> downloads as text -> skips if empty.\n👉 Pick your folder in the trigger.",
        "height": 140,
        "width": 846,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -720,
        -236
      ],
      "id": "sticky-Note-1---Input",
      "name": "Note 1 - Input"
    },
    {
      "parameters": {
        "content": "## (2) Extract tasks (AI)\nAI reads the notes and lists the tasks, owners and priority. The parser keeps the output tidy - keep it.\n👉 Edit the \"Team roles\" list to your team. Swap the model node for any provider.",
        "height": 540,
        "width": 392,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        150,
        -236
      ],
      "id": "sticky-Note-2---AI",
      "name": "Note 2 - AI"
    },
    {
      "parameters": {
        "content": "## (3) Team lookup\nLoads your table of **Name → Slack ID**.\n👉 Point the URL at your table (or Airtable / Sheets).",
        "height": 140,
        "width": 244,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        560,
        -236
      ],
      "id": "sticky-Note-3---Team",
      "name": "Note 3 - Team"
    },
    {
      "parameters": {
        "content": "## (4) Sort & write\nGroups each person's tasks (High/Med/Low) into one message.\n👉 Replace the placeholder Slack IDs + CEO name in the code.",
        "height": 140,
        "width": 244,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        824,
        -236
      ],
      "id": "sticky-Note-4---Format",
      "name": "Note 4 - Format"
    },
    {
      "parameters": {
        "content": "## (5) Send\nDMs each person on Slack.\n👉 Connect your Slack (or email / Telegram / Trello).",
        "height": 140,
        "width": 260,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1088,
        -236
      ],
      "id": "sticky-Note-5---Send",
      "name": "Note 5 - Send"
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "value": "PASTE_YOUR_GOOGLE_DRIVE_FOLDER_ID_HERE",
          "mode": "id"
        },
        "event": "fileCreated",
        "options": {
          "fileType": "application/vnd.google-apps.document"
        }
      },
      "type": "n8n-nodes-base.googleDriveTrigger",
      "typeVersion": 1,
      "position": [
        -688,
        -112
      ],
      "id": "9a17f3e0-43d1-4549-a9a6-ab3d42ad4663",
      "name": "Google Drive Trigger"
    },
    {
      "parameters": {
        "url": "={{ $json.exportLinks[\"text/plain\"] }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        -272,
        -112
      ],
      "id": "2166997d-6719-4174-be75-3d1b98dba2b4",
      "name": "Download transcript as text"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.data }}",
        "hasOutputParser": true,
        "messages": {
          "messageValues": [
            {
              "message": "=You are a meeting task extractor. You receive Google Meet transcripts (Gemini notes) and extract all action items.\n\nThe transcript may be in German, English, or mixed. Always output in English.\n\nTeam roles for assignee context (REPLACE this whole list with your own team and their responsibilities):\n- Alex: CEO, business strategy, client relationships, content vision\n- Sam: Operations & automation, systems, database management, all technical/engineering tasks\n- Jordan: Project management, HR\n- Riley: Social media content, video editing\n- Casey: Customer success\n- Morgan: Creative Strategist\n- Everyone else: Video Editor or client\n\nMeeting type classification:\n- \"client_meeting\": Participants include people who are clearly not team members (external clients, partners, prospects).\n- \"one_on_one\": The CEO (Alex) + exactly one team member.\n- \"office_meeting\": Title contains \"office\" or 3-6 team members participating.\n- \"pep_talk\": Title contains \"pep talk\" or 7+ team members participating.\n- \"learning_session\": Training or workshop meetings focused on tools or skills (e.g. Claude Code, editing techniques). Title or content typically references learning, workshop, or tool walkthroughs.\n\nRules:\n- Only extract work-related tasks. Ignore personal, leisure, or lifestyle items (e.g. travel plans, hobbies, fitness, social activities).\n- Extract tasks from the \"Nächste Schritte\" section AND from the full transcript/details if additional action items are mentioned.\n- Do NOT blindly trust the assignee names in the \"Nächste Schritte\" section. Cross-reference with the actual transcript to determine who committed to doing the task. The person who said they would do it or was told to do it is the assignee, not a person merely mentioned in relation to the task.\n- Use team roles to resolve ambiguity: if a task involves automation, infrastructure, or systems, it belongs to your operations person. If it involves editing or content production, it belongs to your editor. And so on.\n- Normalize names to first names: \"Alex Smith\" becomes \"Alex\".\n- If a task has no clear assignee, set assignee to \"unassigned\".\n- Do not invent tasks. Only extract what was explicitly discussed or committed to.\n- Keep descriptions as short as possible — just the core action, no reasoning, no context, no explanation of why. Example: \"Migrate databases from Airtable to Table\" not \"Migrate existing databases from Airtable to Table, updating all references in the repositories accordingly to improve performance and remove rate limits.\"\n- Extract all participant names from the transcript header and normalize them.\n- Classify the meeting type based on the title and participant list.\n- For client meetings: only extract tasks that one of YOUR team members committed to doing. Ignore tasks that are advice, recommendations, or action items for the client's own business. If the CEO tells a client \"you should launch ads this weekend\", that is NOT a task for your team.\n- Write descriptions in clear, plain language. If someone come back to the tasks 3 weeks later he still need to understand what is meant by the task.\n- For learning sessions: only extract concrete action items with clear deliverables or deadlines. Skip general learning takeaways or \"try this out\" suggestions.\n- Skip trivial items: quick lookups, name checks, spelling verifications, or things clearly resolved during the meeting. If it takes under 5 minutes and has no business impact, skip it.\n- Assign priority based on business impact and urgency:\n  \"high\" = has a deadline this week or directly affects clients/revenue (e.g. \"send campaign to the CEO\", \"launch ads by Friday\").\n  \"medium\" = important work with no immediate deadline (e.g. \"build a lead magnet\", \"prepare a presentation for next week\").\n  \"low\" = long-term improvements or exploratory tasks (e.g. \"research service culture\", \"explore new tools\").\n  Order tasks by priority within each level."
            }
          ]
        },
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        192,
        -128
      ],
      "id": "7c32cdad-215c-4697-a0a5-177088a5f526",
      "name": "Basic LLM (Extract Tasks)",
      "retryOnFail": true,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-6",
          "cachedResultName": "Claude Sonnet 4.6"
        },
        "options": {
          "temperature": 0
        }
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        192,
        80
      ],
      "id": "c62a55fd-a050-4b57-9692-c9d0eb05b46b",
      "name": "Sonnet 4.6"
    },
    {
      "parameters": {
        "jsonSchemaExample": "{\n  \"meeting_name\": \"Meeting Title\",\n  \"meeting_date\": \"2026-06-19\",\n  \"meeting_type\": \"one_on_one\",\n  \"participants\": [\"Alex\", \"Sam\"],\n  \"tasks\": [\n    {\n      \"assignee\": \"Name\",\n      \"description\": \"Task description\",\n      \"priority\": \"high\",\n      \"context\": \"Context from discussion\"\n    }\n  ]\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        336,
        80
      ],
      "id": "20bad3a4-9288-4d32-a32c-c7106ce1b031",
      "name": "Structured Output Parser"
    },
    {
      "parameters": {
        "url": "https://YOUR-TEABLE-HOST/api/table/YOUR_TABLE_ID/record",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        544,
        -128
      ],
      "id": "b0e6664f-1c14-4c2e-b509-227c0f61faf2",
      "name": "Fetch Teable Team Members",
      "retryOnFail": true,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Get LLM output\nconst llmOutput = $('Basic LLM (Extract Tasks)').first().json.output;\nconst meetingName = llmOutput.meeting_name;\nconst meetingDate = llmOutput.meeting_date;\nconst meetingType = llmOutput.meeting_type;\nconst participants = llmOutput.participants;\nconst tasks = llmOutput.tasks;\n\n// Get transcript link from Drive trigger\nconst transcriptLink = $('Google Drive Trigger').first().json.webViewLink;\n\n// Get Teable team members\nconst teableRecords = $('Fetch Teable Team Members').first().json.records;\n\n// Build name -> Slack ID map (lowercase keys)\nconst nameToSlack = {};\nfor (const record of teableRecords) {\n  const name = (record.name || '').toLowerCase().trim();\n  const slackId = record.fields?.Slack_User_ID;\n  if (name && slackId) {\n    nameToSlack[name] = slackId;\n  }\n}\n\n// For one_on_one: find the non-CEO employee\nlet employeeSlackId = null;\nif (meetingType === 'one_on_one') {\n  const employee = participants.find(p => p.toLowerCase() !== 'alex'); // 'alex' = your CEO's first name, must match the AI prompt\n  if (employee) {\n    employeeSlackId = nameToSlack[employee.toLowerCase().trim()] || null;\n  }\n}\n\n// Fallback channel mapping\n// REPLACE all of these with YOUR Slack channel or user IDs.\n// They are the fallback destinations: a shared channel for group meeting types,\n// or a person to catch tasks that have no clear owner.\nconst fallbackChannels = {\n  client_meeting: 'SLACK_ID_FOR_CLIENT_MEETING_TASKS',\n  office_meeting: 'SLACK_CHANNEL_ID_OFFICE_MEETING',\n  pep_talk: 'SLACK_CHANNEL_ID_PEP_TALK',\n  one_on_one: employeeSlackId || 'SLACK_FALLBACK_USER_ID',\n  learning_session: 'SLACK_CHANNEL_ID_LEARNING_SESSION'\n};\n\nconst fallback = fallbackChannels[meetingType] || 'SLACK_FALLBACK_USER_ID';\n\nconst output = [];\n\n// Group tasks by resolved target channel. Automation/system work is now routed by\n// assignee like any other task - the Request Intake sub-workflow leg was removed\n// 2026-08-18 (the sub-workflow is retired, and its failure was eating the DMs).\nconst grouped = {};\nfor (const task of tasks) {\n  const assignee = (task.assignee || 'unassigned').toLowerCase().trim();\n  let targetChannel = nameToSlack[assignee];\n\n  if (!targetChannel) {\n    if (meetingType === 'learning_session') continue;\n    targetChannel = fallback;\n  }\n\n  if (!grouped[targetChannel]) {\n    grouped[targetChannel] = [];\n  }\n  grouped[targetChannel].push({ description: task.description, priority: task.priority });\n}\n\n// Build grouped messages: one per channel\nfor (const [channel, taskList] of Object.entries(grouped)) {\n  const high = taskList.filter(t => t.priority === 'high').map(t => `• ${t.description}`);\n  const medium = taskList.filter(t => t.priority === 'medium').map(t => `• ${t.description}`);\n  const low = taskList.filter(t => t.priority === 'low').map(t => `• ${t.description}`);\n\n  let body = '';\n  if (high.length) body += `*High Priority*\\n>${high.join('\\n>')}\\n\\n`;\n  if (medium.length) body += `*Medium Priority*\\n>${medium.join('\\n>')}\\n\\n`;\n  if (low.length) body += `*Low Priority*\\n>${low.join('\\n>')}\\n\\n`;\n\n  const message = `Tasks from last meeting: *${meetingName}* — ${meetingDate}\\n\\n${body}<${transcriptLink}|📄 Full Transcript>`;\n\n  output.push({\n    json: {\n      channel,\n      message\n    }\n  });\n}\n\nreturn output;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        768,
        -128
      ],
      "id": "eacbc37e-2666-44be-8ffc-fc661111d70e",
      "name": "Group Tasks & Format Messages"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "={{ $json.channel }}",
          "mode": "id"
        },
        "text": "={{ $json.message }}",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "unfurl_links": false,
          "unfurl_media": false
        }
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [
        992,
        -128
      ],
      "id": "90dbf37f-0967-4210-a892-c29afd37ceef",
      "name": "DM to Assignee"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "7d8a18a1-fe04-4522-bee7-78ac9a9583e0",
              "leftValue": "={{ $json.data }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        -64,
        -112
      ],
      "id": "ff15e081-1c66-44c3-89af-9743cbe4ac86",
      "name": "Transcript not empty?"
    },
    {
      "parameters": {
        "unit": "minutes"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -480,
        -112
      ],
      "id": "e9c50608-d106-4a80-bf35-0a7f4a3993d3",
      "name": "Wait 5 minutes"
    }
  ],
  "connections": {
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "Wait 5 minutes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download transcript as text": {
      "main": [
        [
          {
            "node": "Transcript not empty?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sonnet 4.6": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM (Extract Tasks)",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Basic LLM (Extract Tasks)",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM (Extract Tasks)": {
      "main": [
        [
          {
            "node": "Fetch Teable Team Members",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Teable Team Members": {
      "main": [
        [
          {
            "node": "Group Tasks & Format Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group Tasks & Format Messages": {
      "main": [
        [
          {
            "node": "DM to Assignee",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcript not empty?": {
      "main": [
        [
          {
            "node": "Basic LLM (Extract Tasks)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Wait 5 minutes": {
      "main": [
        [
          {
            "node": "Download transcript as text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}