{
  "name": "Turn Slack threads into PowerPoint slides with Claude, checked against the thread",
  "nodes": [
    {
      "id": "c6000000-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 PowerPoint slides",
        "formDescription": "Paste the messages, one per line as \"Name: message\". Lines without \"Name:\" are added to the message above. You get a .pptx file 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-pptx"
        }
      },
      "webhookId": "7d1e4b62-9a3c-4f08-b5d7-2c6e8a1f4b93"
    },
    {
      "id": "c6000000-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": "c6000000-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": "e5b28c41-3f7a-4d96-a0c2-8b1d6f3e9a75"
    },
    {
      "id": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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": "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": "c06",
              "name": "font",
              "value": "Calibri",
              "type": "string"
            },
            {
              "id": "c07",
              "name": "font_east_asian",
              "value": "Yu Gothic",
              "type": "string"
            },
            {
              "id": "c08",
              "name": "accent_color",
              "value": "D9622B",
              "type": "string"
            },
            {
              "id": "c09",
              "name": "language_tag",
              "value": "en-US",
              "type": "string"
            },
            {
              "id": "c10",
              "name": "label_background",
              "value": "Background",
              "type": "string"
            },
            {
              "id": "c11",
              "name": "label_points",
              "value": "Discussion points",
              "type": "string"
            },
            {
              "id": "c12",
              "name": "label_decisions",
              "value": "Decisions needed",
              "type": "string"
            },
            {
              "id": "c13",
              "name": "label_next_steps",
              "value": "Next steps",
              "type": "string"
            },
            {
              "id": "c14",
              "name": "label_sources",
              "value": "Where each item came from",
              "type": "string"
            },
            {
              "id": "c15",
              "name": "label_no_owner",
              "value": "owner not stated",
              "type": "string"
            },
            {
              "id": "c16",
              "name": "fallback_title",
              "value": "Meeting brief",
              "type": "string"
            },
            {
              "id": "c17",
              "name": "label_messages",
              "value": " messages",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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": "c6000000-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 = cfg.fallback_title;\n// One slide per section that has items; each item ends with the message numbers it came from.\nconst today = DateTime.now().setZone($('Config').first().json.timezone);\nconst slides = [{ kind: 'title', title, subtitle: (p.purpose ? p.purpose + '\\n' : '') + today.toFormat('yyyy-MM-dd') + ' · ' + msgs.length + cfg.label_messages }];\nconst section = (label, arr, fmt) => { if (arr.length) slides.push({ kind: 'bullets', title: label, bullets: arr.map((x) => ({ text: fmt(x), ref: '[' + x.refs.join(', ') + ']' })) }); };\nsection(cfg.label_background, background, (x) => x.text);\nsection(cfg.label_points, points, (x) => x.text);\nsection(cfg.label_decisions, decisions, (x) => x.text);\nsection(cfg.label_next_steps, next, (x) => x.task + ' — ' + (x.owner || cfg.label_no_owner) + (x.due ? ', ' + x.due : ''));\nconst lines = [...used.keys()].sort((a, c) => a - c).map((k) => '[' + (k + 1) + '] ' + msgs[k].who + (msgs[k].time ? ' (' + msgs[k].time + ')' : '') + ': \"' + used.get(k) + '\"');\nconst pages = Math.ceil(lines.length / 8);\nconst per = Math.ceil(lines.length / Math.max(pages, 1));\nfor (let i = 0; i < pages; i++) slides.push({ kind: 'lines', title: cfg.label_sources + (pages > 1 ? ' (' + (i + 1) + '/' + pages + ')' : ''), lines: lines.slice(i * per, i * per + per) });\nconst counts = { background: background.length, points: points.length, decisions: decisions.length, next_steps: next.length };\n// File name without characters that Windows, macOS or Slack refuse\nconst file_name = ('Brief ' + today.toFormat('yyyy-MM-dd') + ' ' + title.slice(0, 60)).replace(/[\\\\/:*?\"<>|\\r\\n\\t]+/g, ' ').replace(/\\s+/g, ' ').trim() + '.pptx';\nreturn [{ json: { ok: true, title, deck: { title, slides }, dropped, counts, note: p.note, file_name } }];"
      }
    },
    {
      "id": "c6000000-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": "c6000000-0000-4000-8000-000000000018",
      "name": "Write the .pptx parts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3080,
        -100
      ],
      "parameters": {
        "jsCode": "// Build a .pptx (Office Open XML) with no external libraries: every part of the package is one binary field,\n// and the next node zips them. Fonts and the accent color come from Config.\nconst deck = $json.deck;\nconst cfg = $('Config').first().json;\n\nconst W = 12192000, H = 6858000; // 16:9\nconst FONT = cfg.font || 'Calibri';\nconst FONT_EA = cfg.font_east_asian || FONT;\nconst ACCENT = String(cfg.accent_color || 'D9622B').replace('#', '').toUpperCase();\nconst INK = '23211D', SUB = '6B665D';\nconst NS = 'xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:p=\"http://schemas.openxmlformats.org/presentationml/2006/main\"';\nconst HEAD = '<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n';\nconst esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';\nconst rels = (list) => HEAD + '<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">' +\n  list.map(([id, type, target]) => `<Relationship Id=\"${id}\" Type=\"${type}\" Target=\"${target}\"/>`).join('') + '</Relationships>';\nconst emptyTree = '<p:nvGrpSpPr><p:cNvPr id=\"1\" name=\"\"/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x=\"0\" y=\"0\"/><a:ext cx=\"0\" cy=\"0\"/><a:chOff x=\"0\" y=\"0\"/><a:chExt cx=\"0\" cy=\"0\"/></a:xfrm></p:grpSpPr>';\n\nconst run = (text, sz, color, bold) =>\n  `<a:r><a:rPr lang=\"${cfg.language_tag || 'en-US'}\" sz=\"${sz}\"${bold ? ' b=\"1\"' : ''} dirty=\"0\"><a:solidFill><a:srgbClr val=\"${color}\"/></a:solidFill><a:latin typeface=\"${esc(FONT)}\"/><a:ea typeface=\"${esc(FONT_EA)}\"/></a:rPr><a:t>${esc(text)}</a:t></a:r>`;\nconst textBox = (id, name, x, y, cx, cy, paras) =>\n  `<p:sp><p:nvSpPr><p:cNvPr id=\"${id}\" name=\"${name}\"/><p:cNvSpPr txBox=\"1\"/><p:nvPr/></p:nvSpPr>` +\n  `<p:spPr><a:xfrm><a:off x=\"${x}\" y=\"${y}\"/><a:ext cx=\"${cx}\" cy=\"${cy}\"/></a:xfrm><a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom><a:noFill/></p:spPr>` +\n  `<p:txBody><a:bodyPr wrap=\"square\" rtlCol=\"0\"><a:normAutofit/></a:bodyPr><a:lstStyle/>${paras}</p:txBody></p:sp>`;\nconst bar = (id, x, y, cx, cy, color) =>\n  `<p:sp><p:nvSpPr><p:cNvPr id=\"${id}\" name=\"Bar ${id}\"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x=\"${x}\" y=\"${y}\"/><a:ext cx=\"${cx}\" cy=\"${cy}\"/></a:xfrm><a:prstGeom prst=\"rect\"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val=\"${color}\"/></a:solidFill><a:ln><a:noFill/></a:ln></p:spPr></p:sp>`;\n\nfunction slideXml(s) {\n  let shapes = '';\n  if (s.kind === 'title') {\n    shapes += bar(2, 0, 0, W, 120000, ACCENT);\n    shapes += textBox(3, 'Title', 800000, 2100000, W - 1600000, 1500000, `<a:p>${run(s.title, 4000, INK, true)}</a:p>`);\n    if (s.subtitle) shapes += textBox(4, 'Subtitle', 800000, 3700000, W - 1600000, 1200000,\n      String(s.subtitle).split('\\n').map((l) => `<a:p>${run(l, 1800, SUB)}</a:p>`).join(''));\n  } else {\n    shapes += bar(2, 800000, 600000, 120000, 640000, ACCENT);\n    shapes += textBox(3, 'Title', 1050000, 560000, W - 1850000, 760000, `<a:p>${run(s.title, 2800, INK, true)}</a:p>`);\n    let paras = '';\n    if (s.kind === 'bullets') {\n      const sz = s.bullets.length > 4 ? 1800 : 2000;\n      paras = s.bullets.map((b) => `<a:p><a:pPr marL=\"342900\" indent=\"-342900\"><a:spcBef><a:spcPts val=\"1200\"/></a:spcBef><a:buFont typeface=\"Arial\"/><a:buChar char=\"&#8226;\"/></a:pPr>${run(b.text, sz, INK)}${b.ref ? run('  ' + b.ref, 1200, SUB) : ''}</a:p>`).join('');\n    } else {\n      paras = s.lines.map((l) => `<a:p><a:pPr><a:spcBef><a:spcPts val=\"900\"/></a:spcBef></a:pPr>${run(l, 1400, INK)}</a:p>`).join('');\n    }\n    shapes += textBox(4, 'Body', 1050000, 1600000, W - 1850000, H - 2200000, paras || '<a:p><a:endParaRPr lang=\"en-US\"/></a:p>');\n  }\n  return HEAD + `<p:sld ${NS}><p:cSld><p:spTree>${emptyTree}${shapes}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;\n}\n\nconst clr = (n, v) => `<a:${n}><a:srgbClr val=\"${v}\"/></a:${n}>`;\nconst theme = HEAD + `<a:theme xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" name=\"n8n\"><a:themeElements>` +\n  `<a:clrScheme name=\"n8n\"><a:dk1><a:sysClr val=\"windowText\" lastClr=\"000000\"/></a:dk1><a:lt1><a:sysClr val=\"window\" lastClr=\"FFFFFF\"/></a:lt1>` +\n  clr('dk2', '23211D') + clr('lt2', 'F7F5F1') + clr('accent1', ACCENT) + clr('accent2', '2F7D57') + clr('accent3', '4472C4') +\n  clr('accent4', 'A86A12') + clr('accent5', '6B665D') + clr('accent6', '7A5195') + clr('hlink', '0563C1') + clr('folHlink', '954F72') + `</a:clrScheme>` +\n  `<a:fontScheme name=\"n8n\"><a:majorFont><a:latin typeface=\"${esc(FONT)}\"/><a:ea typeface=\"${esc(FONT_EA)}\"/><a:cs typeface=\"\"/></a:majorFont><a:minorFont><a:latin typeface=\"${esc(FONT)}\"/><a:ea typeface=\"${esc(FONT_EA)}\"/><a:cs typeface=\"\"/></a:minorFont></a:fontScheme>` +\n  `<a:fmtScheme name=\"n8n\"><a:fillStyleLst>${'<a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill>'.repeat(3)}</a:fillStyleLst>` +\n  `<a:lnStyleLst>${'<a:ln w=\"6350\"><a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill></a:ln>'.repeat(3)}</a:lnStyleLst>` +\n  `<a:effectStyleLst>${'<a:effectStyle><a:effectLst/></a:effectStyle>'.repeat(3)}</a:effectStyleLst>` +\n  `<a:bgFillStyleLst>${'<a:solidFill><a:schemeClr val=\"phClr\"/></a:solidFill>'.repeat(3)}</a:bgFillStyleLst></a:fmtScheme>` +\n  `</a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>`;\n\nconst n = deck.slides.length;\nconst files = {};\nconst CT = 'application/vnd.openxmlformats-officedocument';\nfiles['[Content_Types].xml'] = HEAD + '<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">' +\n  '<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/><Default Extension=\"xml\" ContentType=\"application/xml\"/>' +\n  `<Override PartName=\"/ppt/presentation.xml\" ContentType=\"${CT}.presentationml.presentation.main+xml\"/>` +\n  `<Override PartName=\"/ppt/slideMasters/slideMaster1.xml\" ContentType=\"${CT}.presentationml.slideMaster+xml\"/>` +\n  `<Override PartName=\"/ppt/slideLayouts/slideLayout1.xml\" ContentType=\"${CT}.presentationml.slideLayout+xml\"/>` +\n  `<Override PartName=\"/ppt/theme/theme1.xml\" ContentType=\"${CT}.theme+xml\"/>` +\n  `<Override PartName=\"/ppt/presProps.xml\" ContentType=\"${CT}.presentationml.presProps+xml\"/>` +\n  `<Override PartName=\"/ppt/viewProps.xml\" ContentType=\"${CT}.presentationml.viewProps+xml\"/>` +\n  `<Override PartName=\"/ppt/tableStyles.xml\" ContentType=\"${CT}.presentationml.tableStyles+xml\"/>` +\n  `<Override PartName=\"/docProps/core.xml\" ContentType=\"application/vnd.openxmlformats-package.core-properties+xml\"/>` +\n  `<Override PartName=\"/docProps/app.xml\" ContentType=\"${CT}.extended-properties+xml\"/>` +\n  deck.slides.map((_, i) => `<Override PartName=\"/ppt/slides/slide${i + 1}.xml\" ContentType=\"${CT}.presentationml.slide+xml\"/>`).join('') + '</Types>';\nfiles['_rels/.rels'] = rels([\n  ['rId1', `${REL}/officeDocument`, 'ppt/presentation.xml'],\n  ['rId2', 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', 'docProps/core.xml'],\n  ['rId3', `${REL}/extended-properties`, 'docProps/app.xml'],\n]);\nfiles['docProps/core.xml'] = HEAD + '<cp:coreProperties xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">' +\n  `<dc:title>${esc(deck.title)}</dc:title><dc:creator>n8n</dc:creator><dcterms:created xsi:type=\"dcterms:W3CDTF\">${new Date().toISOString().replace(/\\.\\d+Z$/, 'Z')}</dcterms:created></cp:coreProperties>`;\nfiles['docProps/app.xml'] = HEAD + `<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\"><Application>n8n</Application><Slides>${n}</Slides></Properties>`;\nfiles['ppt/presentation.xml'] = HEAD + `<p:presentation ${NS} saveSubsetFonts=\"1\"><p:sldMasterIdLst><p:sldMasterId id=\"2147483648\" r:id=\"rId1\"/></p:sldMasterIdLst>` +\n  `<p:sldIdLst>${deck.slides.map((_, i) => `<p:sldId id=\"${256 + i}\" r:id=\"rId${10 + i}\"/>`).join('')}</p:sldIdLst>` +\n  `<p:sldSz cx=\"${W}\" cy=\"${H}\"/><p:notesSz cx=\"6858000\" cy=\"9144000\"/></p:presentation>`;\nfiles['ppt/_rels/presentation.xml.rels'] = rels([\n  ['rId1', `${REL}/slideMaster`, 'slideMasters/slideMaster1.xml'],\n  ['rId2', `${REL}/presProps`, 'presProps.xml'],\n  ['rId3', `${REL}/viewProps`, 'viewProps.xml'],\n  ['rId4', `${REL}/theme`, 'theme/theme1.xml'],\n  ['rId5', `${REL}/tableStyles`, 'tableStyles.xml'],\n  ...deck.slides.map((_, i) => [`rId${10 + i}`, `${REL}/slide`, `slides/slide${i + 1}.xml`]),\n]);\nfiles['ppt/presProps.xml'] = HEAD + `<p:presentationPr ${NS}/>`;\nfiles['ppt/viewProps.xml'] = HEAD + `<p:viewPr ${NS}><p:normalViewPr><p:restoredLeft sz=\"15620\"/><p:restoredTop sz=\"94660\"/></p:normalViewPr><p:gridSpacing cx=\"76200\" cy=\"76200\"/></p:viewPr>`;\nfiles['ppt/tableStyles.xml'] = HEAD + '<a:tblStyleLst xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" def=\"{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}\"/>';\nfiles['ppt/theme/theme1.xml'] = theme;\nfiles['ppt/slideMasters/slideMaster1.xml'] = HEAD + `<p:sldMaster ${NS}><p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val=\"FFFFFF\"/></a:solidFill><a:effectLst/></p:bgPr></p:bg><p:spTree>${emptyTree}</p:spTree></p:cSld>` +\n  '<p:clrMap bg1=\"lt1\" tx1=\"dk1\" bg2=\"lt2\" tx2=\"dk2\" accent1=\"accent1\" accent2=\"accent2\" accent3=\"accent3\" accent4=\"accent4\" accent5=\"accent5\" accent6=\"accent6\" hlink=\"hlink\" folHlink=\"folHlink\"/>' +\n  '<p:sldLayoutIdLst><p:sldLayoutId id=\"2147483649\" r:id=\"rId1\"/></p:sldLayoutIdLst>' +\n  '<p:txStyles><p:titleStyle><a:lvl1pPr><a:defRPr sz=\"3200\"/></a:lvl1pPr></p:titleStyle><p:bodyStyle><a:lvl1pPr><a:defRPr sz=\"1800\"/></a:lvl1pPr></p:bodyStyle><p:otherStyle><a:lvl1pPr><a:defRPr sz=\"1800\"/></a:lvl1pPr></p:otherStyle></p:txStyles></p:sldMaster>';\nfiles['ppt/slideMasters/_rels/slideMaster1.xml.rels'] = rels([\n  ['rId1', `${REL}/slideLayout`, '../slideLayouts/slideLayout1.xml'],\n  ['rId2', `${REL}/theme`, '../theme/theme1.xml'],\n]);\nfiles['ppt/slideLayouts/slideLayout1.xml'] = HEAD + `<p:sldLayout ${NS} type=\"blank\" preserve=\"1\"><p:cSld name=\"Blank\"><p:spTree>${emptyTree}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`;\nfiles['ppt/slideLayouts/_rels/slideLayout1.xml.rels'] = rels([['rId1', `${REL}/slideMaster`, '../slideMasters/slideMaster1.xml']]);\ndeck.slides.forEach((s, i) => {\n  files[`ppt/slides/slide${i + 1}.xml`] = slideXml(s);\n  files[`ppt/slides/_rels/slide${i + 1}.xml.rels`] = rels([['rId1', `${REL}/slideLayout`, '../slideLayouts/slideLayout1.xml']]);\n});\n\nconst binary = {};\nObject.entries(files).forEach(([path, xml], k) => {\n  binary[`part${k}`] = { data: Buffer.from(xml, 'utf8').toString('base64'), mimeType: 'application/xml', fileName: path };\n});\nreturn [{ json: { file_name: $json.file_name, slides: n, parts: Object.keys(files).length }, binary }];\n"
      }
    },
    {
      "id": "c6000000-0000-4000-8000-000000000019",
      "name": "Zip into a .pptx file",
      "type": "n8n-nodes-base.compression",
      "typeVersion": 1.1,
      "position": [
        3300,
        -100
      ],
      "parameters": {
        "operation": "compress",
        "binaryPropertyName": "={{ Object.keys($binary).join(',') }}",
        "outputFormat": "zip",
        "fileName": "={{ $json.file_name }}",
        "binaryPropertyOutput": "data"
      }
    },
    {
      "id": "c6000000-0000-4000-8000-000000000020",
      "name": "Explain the failure",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        160
      ],
      "parameters": {
        "jsCode": "const err = $json.error && ($json.error.message || $json.error);\nreturn [{ json: { problem: 'An error came back while reading the Slack thread' + (err ? ' (' + String(err).slice(0, 200) + ')' : '') + '.' } }];"
      }
    },
    {
      "id": "c6000000-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": "c6000000-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": "c6000000-0000-4000-8000-000000000023",
      "name": "Write the Slack reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3740,
        400
      ],
      "parameters": {
        "jsCode": "// The comment that goes with the file: what went into the slides, and what was left out and why.\n// When there is a file, it is passed on so the next node can attach it.\nconst m = $('Read the Slack mention').first().json;\nif ($json.problem) return [{ json: { channel: m.channel, thread_ts: m.thread_ts, has_file: false, text: 'No slides were made. ' + $json.problem } }];\nconst b = $('Check the brief').all(0)[0].json;\nconst c = b.counts;\nconst pl = (k, one, many) => k + ' ' + (k === 1 ? one : many);\nconst text = '*Slides ready:* ' + b.title + '\\n' + [pl(c.background, 'background item', 'background items'), pl(c.points, 'discussion point', 'discussion points'), pl(c.decisions, 'decision needed', 'decisions needed'), pl(c.next_steps, 'next step', 'next steps')].join(', ') + '. 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.';\nreturn [{ json: { channel: m.channel, thread_ts: m.thread_ts, has_file: true, text, file_name: b.file_name }, binary: $input.first().binary }];"
      }
    },
    {
      "id": "c6000000-0000-4000-8000-000000000024",
      "name": "A file to attach?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3960,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "f",
              "leftValue": "={{ $json.has_file }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "c6000000-0000-4000-8000-000000000025",
      "name": "Upload to the thread",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        4180,
        200
      ],
      "parameters": {
        "resource": "file",
        "operation": "upload",
        "binaryPropertyName": "data",
        "options": {
          "channelId": "={{ $json.channel }}",
          "threadTs": "={{ $json.thread_ts }}",
          "initialComment": "={{ $json.text }}",
          "fileName": "={{ $json.file_name }}",
          "title": "={{ $json.file_name }}"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "c6000000-0000-4000-8000-000000000026",
      "name": "Upload failed: say so",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4400,
        200
      ],
      "parameters": {
        "jsCode": "// The file could not be attached (often a missing files:write scope). Say so in the thread instead.\nconst r = $('Write the Slack reply').first().json;\nconst err = $json.error && ($json.error.message || $json.error);\nreturn [{ json: { channel: r.channel, thread_ts: r.thread_ts, text: 'The slides were made, but the file could not be attached' + (err ? ' (' + String(err).slice(0, 200) + ')' : '') + '. Check that the Slack app has the files:write scope.' } }];"
      }
    },
    {
      "id": "c6000000-0000-4000-8000-000000000027",
      "name": "Reply in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        4620,
        480
      ],
      "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": "c6000000-0000-4000-8000-000000000028",
      "name": "Done",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.5,
      "position": [
        3740,
        -200
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "returnBinary",
        "inputDataFieldName": "data",
        "completionTitle": "={{ 'Slides ready: ' + $('Check the brief').all(0)[0].json.title }}",
        "completionMessage": "={{ 'Your .pptx file is downloading.' + ($('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": "9f4c2a7e-6b1d-4e83-9c05-3a7e1d8b2f46"
    },
    {
      "id": "c6000000-0000-4000-8000-000000000029",
      "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": "1b8e5d3a-7c2f-4a69-8e14-6d9b2c5f7a30"
    },
    {
      "parameters": {
        "content": "## Turn a Slack thread into a PowerPoint file, 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**: 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.\n\n**Code then writes the .pptx itself** (Office Open XML, zipped by the Compression node). No Google or Microsoft account, no community node. The file is attached in the thread, or downloaded from the form.\n\n### Setup\n1. Credentials: Slack (bot token), Anthropic.\n2. Slack app: bot scopes app_mentions:read, channels:history, chat:write, files:write, users:read; Event Subscriptions with the production URL of **Slack mention**, subscribed to app_mention.\n3. List member IDs in **Slack settings**. Set fonts, accent color and labels in **Config**. Try the form first.",
        "height": 1080,
        "width": 580,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -640,
        -260
      ],
      "id": "c6000000-0000-4000-9000-000000000029",
      "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": "c6000000-0000-4000-9000-000000000030",
      "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": "c6000000-0000-4000-9000-000000000031",
      "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": "c6000000-0000-4000-9000-000000000032",
      "name": "Section 3"
    },
    {
      "parameters": {
        "content": "## 4. The .pptx file\nOne slide per section with items, then the quotes. Code writes each XML part; the Compression node zips them into a file PowerPoint opens.",
        "height": 620,
        "width": 420,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3060,
        -260
      ],
      "id": "c6000000-0000-4000-9000-000000000033",
      "name": "Section 4"
    },
    {
      "parameters": {
        "content": "## 5. The answer\nSlack: the file in the thread with what went in and what was left out (if the upload fails, the reason is posted instead). Form: the file downloads from the result page.",
        "height": 1100,
        "width": 1340,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3500,
        -360
      ],
      "id": "c6000000-0000-4000-9000-000000000034",
      "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": "Write the .pptx parts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Problem: answer where asked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write the .pptx parts": {
      "main": [
        [
          {
            "node": "Zip into a .pptx file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zip into a .pptx file": {
      "main": [
        [
          {
            "node": "Answer where asked",
            "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": "A file to attach?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "A file to attach?": {
      "main": [
        [
          {
            "node": "Upload to the thread",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reply in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to the thread": {
      "main": [
        [],
        [
          {
            "node": "Upload failed: say so",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload failed: say so": {
      "main": [
        [
          {
            "node": "Reply in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {}
}