{
  "name": "Turn Slack threads into meeting brief slides with Claude and Google Slides",
  "nodes": [
    {
      "id": "c5000000-0000-4000-8000-000000000001",
      "name": "Paste a conversation",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.6,
      "position": [
        0,
        0
      ],
      "parameters": {
        "formTitle": "Turn a conversation into slides",
        "formDescription": "Paste the messages, one per line as \"Name: message\". Lines without \"Name:\" are added to the message above. You get a copy of your Slides template with background, discussion points, decisions needed and next steps.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Conversation",
              "fieldName": "Conversation",
              "fieldType": "textarea",
              "requiredField": true,
              "placeholder": "Sato: Can we confirm the booth size by Friday?\nTanaka: Yes, 3m x 3m. Budget is 300,000 yen."
            },
            {
              "fieldLabel": "What the slides are for",
              "fieldName": "Purpose",
              "fieldType": "text",
              "requiredField": false,
              "placeholder": "For the weekly team meeting"
            }
          ]
        },
        "responseMode": "lastNode",
        "options": {
          "path": "conversation-to-slides"
        }
      },
      "webhookId": "4b7e2a19-6c3d-4f85-9a10-3e5d7c2b8f64"
    },
    {
      "id": "c5000000-0000-4000-8000-000000000002",
      "name": "Read the pasted conversation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "// \"Name: message\" per line (also the full-width colon). Lines without a name continue the message above.\nconst raw = String($json.Conversation || '').replace(/\\r/g, '');\nconst messages = [];\nfor (const line of raw.split('\\n')) {\n  const t = line.trim();\n  if (!t) continue;\n  const m = t.match(/^([^:：]{1,40})[:：]\\s*(.+)$/);\n  if (m && !/^https?$/i.test(m[1])) messages.push({ who: m[1].trim(), text: m[2].trim(), time: '' });\n  else if (messages.length) messages[messages.length - 1].text += '\\n' + t;\n  else messages.push({ who: '(unknown)', text: t, time: '' });\n}\nreturn [{ json: { source: 'form', purpose: String($json.Purpose || '').trim(), messages, where: 'pasted conversation' } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000003",
      "name": "Slack mention",
      "type": "n8n-nodes-base.slackTrigger",
      "typeVersion": 1,
      "position": [
        0,
        400
      ],
      "parameters": {
        "trigger": [
          "app_mention"
        ],
        "watchWorkspace": true,
        "options": {}
      },
      "webhookId": "d3a91c5e-7f24-4b68-8e0d-5c2f9a6b1e47"
    },
    {
      "id": "c5000000-0000-4000-8000-000000000004",
      "name": "Slack settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        400
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "s01",
              "name": "allowed_slack_users",
              "value": "U0123456789",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000005",
      "name": "Read the Slack mention",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        400
      ],
      "parameters": {
        "jsCode": "// The mention must be inside a thread; the text after the mention says what the slides are for.\nconst ev = $json.event || $json;\nif (ev.bot_id) return [];\nconst allowed = String($json.allowed_slack_users || '').split(',').map((s) => s.trim()).filter(Boolean);\nconst appMention = (String(ev.text || '').match(/<@[A-Z0-9]+>/) || [''])[0]; // the app's own mention, to leave earlier requests out of the conversation\nconst base = { source: 'slack', channel: ev.channel, thread_ts: ev.thread_ts || ev.ts, mention_ts: ev.ts, app_mention: appMention };\nlet text = '';\nif (!allowed.includes(ev.user)) text = 'Sorry, you are not on the list of people who can use this.';\nelse if (!ev.thread_ts) text = 'Mention me inside a thread (reply to the conversation), and I will turn that thread into slides.';\n// the words after the mention, without Slack formatting marks\nconst purpose = String(ev.text || '').replace(/<@[^>]+>/g, ' ').replace(/[*_~`]/g, '').replace(/\\s+/g, ' ').trim();\nreturn [{ json: { ok: !text, text, purpose, ...base } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000006",
      "name": "Inside a thread?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        660,
        400
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000007",
      "name": "Read the thread",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        880,
        400
      ],
      "parameters": {
        "resource": "channel",
        "operation": "replies",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.channel }}"
        },
        "ts": "={{ $json.thread_ts }}",
        "returnAll": true,
        "filters": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c5000000-0000-4000-8000-000000000008",
      "name": "List the people",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        400
      ],
      "parameters": {
        "jsCode": "// One item per person who wrote in the thread (bots and the mention itself are left out later).\nconst ids = [...new Set($input.all().map((i) => i.json.user).filter(Boolean))];\nreturn ids.map((user) => ({ json: { user } }));"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000009",
      "name": "Look up the names",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1320,
        400
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://slack.com/api/users.info?user={{ $json.user }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "slackApi",
        "options": {}
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000010",
      "name": "Build the Slack conversation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        400
      ],
      "parameters": {
        "jsCode": "// Names come from users.info (needs the users:read scope). Without it, people are shown as Member A, B, ...\nconst m = $('Read the Slack mention').first().json;\nconst people = $('List the people').all().map((i) => i.json.user);\nconst names = {};\n$input.all().forEach((i, k) => {\n  const u = i.json.user;\n  const name = i.json.ok && u ? (u.profile && (u.profile.display_name || u.profile.real_name)) || u.real_name || u.name : null;\n  names[people[k]] = name || 'Member ' + String.fromCharCode(65 + k);\n});\nconst messages = $('Read the thread').all().map((i) => i.json)\n  .filter((x) => !x.bot_id && x.ts !== m.mention_ts && String(x.text || '').trim() && !(m.app_mention && String(x.text).includes(m.app_mention)))\n  .map((x) => ({ who: names[x.user] || 'Unknown', text: String(x.text).replace(/<@([A-Z0-9]+)>/g, (s, id) => '@' + (names[id] || id)), ts: Number(x.ts) }));\nreturn [{ json: { source: 'slack', purpose: m.purpose, messages, channel: m.channel, thread_ts: m.thread_ts, where: 'Slack thread' } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000011",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "mode": "manual",
        "includeOtherFields": true,
        "assignments": {
          "assignments": [
            {
              "id": "c01",
              "name": "template_id",
              "value": "PASTE_YOUR_SLIDES_TEMPLATE_ID",
              "type": "string"
            },
            {
              "id": "c02",
              "name": "max_items",
              "value": 5,
              "type": "number"
            },
            {
              "id": "c03",
              "name": "max_messages",
              "value": 200,
              "type": "number"
            },
            {
              "id": "c04",
              "name": "timezone",
              "value": "Asia/Tokyo",
              "type": "string"
            },
            {
              "id": "c05",
              "name": "default_purpose",
              "value": "A short brief for the next team meeting",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000012",
      "name": "Prepare the prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        0
      ],
      "parameters": {
        "jsCode": "// Numbered transcript for Claude. Too short a conversation is stopped here.\nconst c = $json;\n// Slack times are shown in the Config time zone\nconst msgs = (c.messages || []).slice(-Number(c.max_messages)).map((m) => ({ who: m.who, text: m.text, time: m.ts ? DateTime.fromSeconds(m.ts).setZone(c.timezone).toFormat('yyyy-MM-dd HH:mm') : m.time || '' }));\nif (msgs.length < 2) return [{ json: { ok: false, problem: 'There are fewer than 2 messages to work from.' } }];\nconst transcript = msgs.map((m, i) => '[' + (i + 1) + '] ' + m.who + (m.time ? ' (' + m.time + ')' : '') + ': ' + m.text.replace(/\\n/g, ' / ')).join('\\n');\nconst note = (c.messages || []).length > msgs.length ? 'Only the last ' + msgs.length + ' messages were used.' : '';\nreturn [{ json: { ok: true, messages: msgs, transcript, purpose: c.purpose || c.default_purpose, note } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000013",
      "name": "Enough to work with?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2200,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000014",
      "name": "Claude Haiku 4.5",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.5,
      "position": [
        2420,
        220
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-haiku-4-5-20251001",
          "cachedResultName": "Claude Haiku 4.5"
        },
        "options": {}
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000015",
      "name": "Write the brief",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        2420,
        0
      ],
      "parameters": {
        "promptType": "define",
        "text": "={{ 'Purpose: ' + $json.purpose + '\\n\\nConversation:\\n' + $json.transcript }}",
        "messages": {
          "messageValues": [
            {
              "type": "SystemMessagePromptTemplate",
              "message": "You turn a team conversation into a short brief for slides.\nUse only what the conversation says. Do not add facts, numbers, dates, names or deadlines that are not written in it, and do not convert relative dates (\"next Friday\") into calendar dates.\nKeep how certain each statement is: an idea, plan or proposal stays an idea, plan or proposal (\"plans to\", \"proposes\"); do not turn it into a decision.\nWrite in the same language as the conversation. Keep each item to one short sentence. At most 5 items per section; leave a section empty if the conversation says nothing for it.\nFor every item, list the messages it is based on: for each, a short exact quote (a few words, copied character by character) and the message number. If an item combines facts from several messages, list each of them. Every number, date or deadline in an item must be inside one of its quotes (for example, if the task mentions \"500\" and the deadline is in another message, quote both messages).\n\"title\" names the topic of the conversation in a few words (not the purpose of the slides).\nReturn only JSON:\n{\"title\": \"...\",\n \"background\": [{\"text\": \"...\", \"sources\": [{\"quote\": \"...\", \"msg\": 1}]}],\n \"points\": [{\"text\": \"...\", \"sources\": [{\"quote\": \"...\", \"msg\": 1}]}],\n \"decisions\": [{\"text\": \"...\", \"sources\": [{\"quote\": \"...\", \"msg\": 1}]}],\n \"next_steps\": [{\"task\": \"...\", \"owner\": \"name as written, or null\", \"due\": \"as written, or null\", \"sources\": [{\"quote\": \"...\", \"msg\": 1}]}]}\n\"points\" are the open questions and options discussed. \"decisions\" are the things the team still needs to decide. \"next_steps\" are actions someone agreed to do."
            }
          ]
        }
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000016",
      "name": "Check the brief",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2640,
        0
      ],
      "parameters": {
        "jsCode": "// Every item must quote a message that really says it, and every number or date in it must be written in that message.\nconst p = $('Prepare the prompt').first().json;\nconst cfg = $('Config').first().json;\nconst msgs = p.messages;\nconst norm = (s) => String(s ?? '').normalize('NFKC').toLowerCase().replace(/\\s+/g, ' ').replace(/[「」『』\"“”'’]/g, '').trim();\nconst digits = (s) => String(s ?? '').normalize('NFKC').match(/\\d+/g) || [];\nconst hasNum = (text, t) => new RegExp('(^|\\\\D)0*' + Number(t) + '(\\\\D|$)').test(String(text).normalize('NFKC'));\nlet b;\ntry { b = JSON.parse(String($json.text ?? '').match(/\\{[\\s\\S]*\\}/)[0]); }\ncatch (e) { return [{ json: { ok: false, problem: 'Could not read the AI answer as JSON. Try again.' } }]; }\nconst dropped = [];\nconst used = new Map();\nfunction findMsg(s) {\n  const q = norm(s && s.quote);\n  if (q.length < 3) return null;\n  const k = Number(s.msg) - 1;\n  if (msgs[k] && norm(msgs[k].text).includes(q)) return k;\n  const other = msgs.findIndex((m) => norm(m.text).includes(q));\n  return other >= 0 ? other : null;\n}\nfunction check(section, it, fields) {\n  const main = fields[0];\n  const label = fields.map((f) => it[f]).filter(Boolean).join(' / ');\n  const srcs = Array.isArray(it.sources) ? it.sources : it.quote ? [{ quote: it.quote, msg: it.msg }] : [];\n  const ks = srcs.map(findMsg);\n  if (!ks.length || ks.some((k) => k === null)) { dropped.push(section + ': \"' + label + '\" (a quote it gives is not in the conversation)'); return null; }\n  const where = [...new Set(ks)].sort((a, b) => a - b);\n  const src = where.map((k) => msgs[k].text).join('\\n');\n  const nums = where.map((k) => k + 1).join(', ');\n  const bad = digits(it[main]).filter((t) => !hasNum(src, t));\n  if (bad.length) { dropped.push(section + ': \"' + label + '\" (' + bad.join(', ') + ' is not in message ' + nums + ')'); return null; }\n  const out = { ...it, refs: where.map((k) => k + 1) };\n  if (it.due && (digits(it.due).length ? digits(it.due).some((t) => !hasNum(src, t)) : !norm(src).includes(norm(it.due)))) {\n    dropped.push(section + ': the due date \"' + it.due + '\" of \"' + it[main] + '\" (not written in message ' + nums + ')'); out.due = null;\n  }\n  if (it.owner) {\n    const o = norm(it.owner);\n    const named = msgs.some((m) => norm(m.who) === o) || norm(src).includes(o);\n    if (!named) { dropped.push(section + ': the owner \"' + it.owner + '\" of \"' + it[main] + '\" (not a person in the conversation)'); out.owner = null; }\n  }\n  srcs.forEach((s, i) => { if (!used.has(ks[i])) used.set(ks[i], String(s.quote || '').trim()); });\n  return out;\n}\nconst max = Number(cfg.max_items);\nconst pick = (section, list, fields) => (Array.isArray(list) ? list : []).slice(0, max).map((it) => check(section, it, fields)).filter(Boolean);\nconst background = pick('Background', b.background, ['text']);\nconst points = pick('Discussion points', b.points, ['text']);\nconst decisions = pick('Decisions needed', b.decisions, ['text']);\nconst next = pick('Next steps', b.next_steps, ['task']);\nif (!background.length && !points.length && !decisions.length && !next.length) return [{ json: { ok: false, problem: 'Nothing in the AI draft could be traced back to the conversation, so no slides were made.' + (dropped.length ? ' Dropped: ' + dropped.slice(0, 3).join('; ') : '') } }];\nlet title = String(b.title || '').trim();\nif (!title || digits(title).some((t) => !msgs.some((m) => hasNum(m.text, t)))) title = 'Meeting brief';\nconst ref = (x) => ' [' + x.refs.join(', ') + ']';\nconst bullets = (arr) => (arr.length ? arr.map((x) => '• ' + x.text + ref(x)).join('\\n') : '(nothing in the conversation)');\nconst today = DateTime.now().setZone($('Config').first().json.timezone);\nconst values = {\n  title,\n  subtitle: (p.purpose ? p.purpose + '\\n' : '') + 'From a ' + ($('Config').first().json.where || 'conversation') + ' (' + msgs.length + ' messages) · ' + today.toFormat('yyyy-MM-dd'),\n  background: bullets(background), points: bullets(points), decisions: bullets(decisions),\n  next_steps: next.length ? next.map((x) => '• ' + x.task + (x.owner ? ' — ' + x.owner : ' — (owner not stated)') + (x.due ? ', ' + x.due : '') + ref(x)).join('\\n') : '(nothing in the conversation)',\n  sources: [...used.keys()].sort((a, c) => a - c).map((k) => '[' + (k + 1) + '] ' + msgs[k].who + (msgs[k].time ? ' (' + msgs[k].time + ')' : '') + ': \"' + used.get(k) + '\"').join('\\n'),\n};\nconst requests = Object.entries(values).map(([k, v]) => ({ replaceAllText: { containsText: { text: '{{' + k + '}}', matchCase: true }, replaceText: String(v) } }));\nconst counts = { background: background.length, points: points.length, decisions: decisions.length, next_steps: next.length };\nreturn [{ json: { ok: true, values, requests, dropped, counts, note: p.note, file_name: 'Brief ' + today.toFormat('yyyy-MM-dd') + ' ' + title.slice(0, 60) } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000017",
      "name": "Anything to show?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2860,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "ok",
              "leftValue": "={{ $json.ok }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000018",
      "name": "Copy the template",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        3080,
        -100
      ],
      "parameters": {
        "resource": "file",
        "operation": "copy",
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Config').first().json.template_id }}"
        },
        "name": "={{ $json.file_name }}",
        "sameFolder": true,
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c5000000-0000-4000-8000-000000000019",
      "name": "Fill in the slides",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        3300,
        -100
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://slides.googleapis.com/v1/presentations/{{ $json.id }}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSlidesOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: $('Check the brief').first().json.requests }) }}",
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c5000000-0000-4000-8000-000000000020",
      "name": "Explain the failure",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        160
      ],
      "parameters": {
        "jsCode": "const labels = { 'Read the thread': 'reading the Slack thread', 'Copy the template': 'copying the Slides template', 'Fill in the slides': 'filling in the slides' };\nconst node = $prevNode.name;\nconst err = $json.error && ($json.error.message || $json.error);\nreturn [{ json: { problem: 'An error came back while ' + (labels[node] || node) + (err ? ' (' + String(err).slice(0, 200) + ')' : '') + '.' + (node === 'Fill in the slides' ? ' An unfinished copy of the template is left in Google Drive; delete it.' : '') } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000021",
      "name": "Answer where asked",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        3520,
        -100
      ],
      "parameters": {
        "mode": "expression",
        "numberOutputs": 2,
        "output": "={{ $('Read the Slack mention').isExecuted ? 1 : 0 }}"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000022",
      "name": "Problem: answer where asked",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.4,
      "position": [
        3520,
        160
      ],
      "parameters": {
        "mode": "expression",
        "numberOutputs": 2,
        "output": "={{ $('Read the Slack mention').isExecuted ? 1 : 0 }}"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000023",
      "name": "Write the Slack reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3740,
        400
      ],
      "parameters": {
        "jsCode": "// Reply in the thread: the link, what went into the slides, and what was left out and why.\nconst m = $('Read the Slack mention').first().json;\nlet text;\nif ($json.problem) text = 'No slides were made. ' + $json.problem;\nelse {\n  const b = $('Check the brief').all(0)[0].json;\n  const url = 'https://docs.google.com/presentation/d/' + $('Copy the template').all(0)[0].json.id + '/edit';\n  const c = b.counts;\n  text = '*Slides ready:* <' + url + '|' + b.values.title + '>\\n' + c.background + ' background, ' + c.points + ' discussion points, ' + c.decisions + ' decisions, ' + c.next_steps + ' next steps. Each item shows the message number it came from.'\n    + (b.dropped.length ? '\\n*Left out* (could not be traced to the conversation):\\n' + b.dropped.slice(0, 8).map((d) => '• ' + d).join('\\n') : '')\n    + (b.note ? '\\n' + b.note : '') + '\\nCheck the slides before you share them.';\n}\nreturn [{ json: { channel: m.channel, thread_ts: m.thread_ts, text } }];"
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000024",
      "name": "Reply in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        3960,
        400
      ],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.channel }}"
        },
        "messageType": "text",
        "text": "={{ $json.text }}",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "unfurl_links": false,
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $json.thread_ts }}"
            }
          }
        }
      }
    },
    {
      "id": "c5000000-0000-4000-8000-000000000025",
      "name": "Done",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        3740,
        -200
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "={{ 'Slides ready: ' + $('Check the brief').all(0)[0].json.values.title }}",
        "completionMessage": "={{ 'https://docs.google.com/presentation/d/' + $('Copy the template').all(0)[0].json.id + '/edit' + ($('Check the brief').all(0)[0].json.dropped.length ? ' — Left out (could not be traced to the conversation): ' + $('Check the brief').all(0)[0].json.dropped.join('; ') : '') }}"
      },
      "webhookId": "8c2f6e1a-4d9b-4a37-b5e0-1f7a3c9d2e58"
    },
    {
      "id": "c5000000-0000-4000-8000-000000000026",
      "name": "Show the problem",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        3740,
        120
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "text",
        "completionTitle": "No slides were made",
        "completionMessage": "={{ $json.problem }}"
      },
      "webhookId": "2e9a5c7f-1b3d-4e60-8f24-7a1c5d9e3b06"
    },
    {
      "parameters": {
        "content": "## Turn a Slack thread into meeting slides, with nothing the thread did not say\nMention the app inside a Slack thread (\"make slides for the weekly meeting\"), or paste a conversation into the form. **Claude fills a fixed outline** — background, discussion points, decisions needed, next steps (who, what, by when) — and **quotes the message behind every item**.\n\n**Code checks each item** before it reaches the slides: the quotes must be in the conversation, every number and date must be written in the quoted messages, owners must be people in the thread. What fails is left out and listed in the reply. Relative deadlines like \"next week\" stay as written.\n\n### Setup\n1. Credentials: Slack (bot token), Google Drive, Google Slides (OAuth2), Anthropic.\n2. A Slides template with the {{placeholders}} from the description (your company layout).\n3. Slack app: bot scopes app_mentions:read, channels:history, chat:write, users:read; Event Subscriptions with the production URL of **Slack mention**, subscribed to app_mention.\n4. Paste the template ID into **Config**, list member IDs in **Slack settings**. Try the form first.",
        "height": 1020,
        "width": 580,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -640,
        -260
      ],
      "id": "c5000000-0000-4000-9000-000000000026",
      "name": "Overview"
    },
    {
      "parameters": {
        "content": "## 1. The conversation comes in\n**Form**: paste \"Name: message\" lines. **Slack**: mention the app inside a thread; the thread is read, people's names are looked up (users:read), bots and earlier requests to the app are left out. The words after the mention say what the slides are for.",
        "height": 920,
        "width": 1740,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -20,
        -260
      ],
      "id": "c5000000-0000-4000-9000-000000000027",
      "name": "Section 1"
    },
    {
      "parameters": {
        "content": "## 2. Claude fills the outline\nNumbered transcript in, JSON out: each item with one or more exact quotes and message numbers. Claude is told not to add facts, dates or names, not to turn relative dates into calendar dates, and not to turn plans into decisions.",
        "height": 620,
        "width": 860,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1740,
        -260
      ],
      "id": "c5000000-0000-4000-9000-000000000028",
      "name": "Section 2"
    },
    {
      "parameters": {
        "content": "## 3. Code checks every item\nQuotes found? Numbers and dates written in those messages? Owner a person in the thread? Failing items or fields are dropped and listed.",
        "height": 620,
        "width": 420,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2620,
        -260
      ],
      "id": "c5000000-0000-4000-9000-000000000029",
      "name": "Section 3"
    },
    {
      "parameters": {
        "content": "## 4. Slides\nCopy your template and replace the {{placeholders}}. Each item ends with the message numbers it came from; the last slide lists the quotes.",
        "height": 620,
        "width": 420,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3060,
        -260
      ],
      "id": "c5000000-0000-4000-9000-000000000030",
      "name": "Section 4"
    },
    {
      "parameters": {
        "content": "## 5. The answer\nIn the Slack thread: the link, what went in, and what was left out and why. On the form: a result page.",
        "height": 1000,
        "width": 700,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3500,
        -360
      ],
      "id": "c5000000-0000-4000-9000-000000000031",
      "name": "Section 5"
    }
  ],
  "connections": {
    "Paste a conversation": {
      "main": [
        [
          {
            "node": "Read the pasted conversation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack mention": {
      "main": [
        [
          {
            "node": "Slack settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack settings": {
      "main": [
        [
          {
            "node": "Read the Slack mention",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the Slack mention": {
      "main": [
        [
          {
            "node": "Inside a thread?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inside a thread?": {
      "main": [
        [
          {
            "node": "Read the thread",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the thread": {
      "main": [
        [
          {
            "node": "List the people",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List the people": {
      "main": [
        [
          {
            "node": "Look up the names",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look up the names": {
      "main": [
        [
          {
            "node": "Build the Slack conversation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the pasted conversation": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the Slack conversation": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Prepare the prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare the prompt": {
      "main": [
        [
          {
            "node": "Enough to work with?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enough to work with?": {
      "main": [
        [
          {
            "node": "Write the brief",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Haiku 4.5": {
      "ai_languageModel": [
        [
          {
            "node": "Write the brief",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Write the brief": {
      "main": [
        [
          {
            "node": "Check the brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the brief": {
      "main": [
        [
          {
            "node": "Anything to show?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything to show?": {
      "main": [
        [
          {
            "node": "Copy the template",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Copy the template": {
      "main": [
        [
          {
            "node": "Fill in the slides",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fill in the slides": {
      "main": [
        [
          {
            "node": "Answer where asked",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Explain the failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Explain the failure": {
      "main": [
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer where asked": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the Slack reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Problem: answer where asked": {
      "main": [
        [
          {
            "node": "Show the problem",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Write the Slack reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the Slack reply": {
      "main": [
        [
          {
            "node": "Reply in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}