{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "7a8bb04e-44f8-4713-b38f-e07b464c025d",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        -224,
        672
      ],
      "parameters": {
        "options": {
          "responseMode": "responseNodes"
        },
        "agentName": "Free Business Strategy Canvas Generator",
        "availableInChat": true,
        "agentDescription": "Guide your business strategy with AI \u2014 create a Business Model Canvas, Lean Canvas, and SWOT Analysis in one free conversation. Includes web research, competitor analysis, and location-aware suggestions.\n",
        "suggestedPrompts": {
          "prompts": [
            {
              "icon": {
                "type": "icon",
                "value": "clipboard-list"
              },
              "text": "Create a Business Model Canvas for my business"
            },
            {
              "icon": {
                "type": "icon",
                "value": "brain"
              },
              "text": "I want to create a Lean Canvas for my startup"
            },
            {
              "icon": {
                "type": "icon",
                "value": "chart-column-decreasing"
              },
              "text": "Run a SWOT Analysis for my business"
            },
            {
              "icon": {
                "type": "icon",
                "value": "check-check"
              },
              "text": "Generate all three canvases for my business"
            }
          ]
        }
      },
      "typeVersion": 1.4
    },
    {
      "id": "460f0153-2a0f-4adb-9e6a-173c668d499c",
      "name": "MongoDB Chat Memory1",
      "type": "@n8n/n8n-nodes-langchain.memoryMongoDbChat",
      "position": [
        352,
        880
      ],
      "parameters": {
        "databaseName": "N8N_Business_model_canvas_generator",
        "contextWindowLength": 100
      },
      "credentials": {
        "mongoDb": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "002b7162-d942-461a-ad9d-d7586bd04a79",
      "name": "tavily_search1",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        128,
        880
      ],
      "parameters": {
        "url": "https://api.tavily.com/search",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"api_key\": \"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\",\n  \"query\": \"{{$fromAI('query', 'The search query')}}\",\n  \"search_depth\": \"basic\",\n  \"max_results\": 5\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.4
    },
    {
      "id": "eba849a5-2f67-4a44-9e43-9ad551cb2d17",
      "name": "tavily_research1",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        -16,
        880
      ],
      "parameters": {
        "url": "https://api.tavily.com/search",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"api_key\": \"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\",\n  \"query\": \"{{$fromAI('query', 'The search query')}}\",\n  \"search_depth\": \"advanced\",\n  \"max_results\": 10\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.4
    },
    {
      "id": "01768a9e-34ce-487b-9c07-e92f80dfb08e",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1360,
        656
      ],
      "parameters": {
        "message": "See attatchment",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "Your generated canvases",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "020a1d70-afa8-4837-a4e6-8003f0702464",
      "name": "Chat",
      "type": "@n8n/n8n-nodes-langchain.chat",
      "position": [
        528,
        688
      ],
      "parameters": {
        "message": "={{ $json.output }}",
        "options": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "f7293912-a749-4fe9-8189-14c7158eb936",
      "name": "Generate canvases",
      "type": "n8n-nodes-base.if",
      "position": [
        336,
        672
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "a9dc9e41-24d2-49f1-a503-ed5537ac6fca",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "[CANVAS_READY]"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "59809f0a-b2bc-4404-8b20-a35ea8813d35",
      "name": "Canvas generator",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -16,
        672
      ],
      "parameters": {
        "text": "={{ $json.chatInput }}",
        "options": {
          "systemMessage": "You are a business strategy consultant and expert in Business Model Canvas (BMC), Lean Canvas, and SWOT Analysis. Your goal is to guide the user through a single focused conversation and generate whichever canvas(es) they select \u2014 with specific, research-backed, non-generic results. You are intentionally nosy and persistent \u2014 you never accept vague answers and always push for more detail.\n\n## Step 1: Canvas selection\nAsk the user which output they want:\n1. Business Model Canvas (BMC) \u2014 best for existing businesses, focuses on operations and revenue\n2. Lean Canvas \u2014 best for startups, focuses on problem-solution fit and key metrics\n3. SWOT Analysis \u2014 best for strategic review, analyzes Strengths, Weaknesses, Opportunities and Threats\n4. All three \u2014 recommended, generates all canvases from one conversation\n\nBriefly explain the difference in one sentence each so the user can make an informed choice.\n\n## Step 2: Discovery\nAsk these questions one at a time before touching any canvas section:\n1. What is your business idea or existing business? Describe it in detail \u2014 what do you do, who do you do it for, and how long have you been doing it?\n2. What country/city are you based in?\n3. Is this a new idea or an existing business? If existing, how long have you been operating?\n4. Who are your 2-3 biggest competitors? If you don't know, say so \u2014 you will research this immediately using tavily_research and name at least 3 real competitors before proceeding.\n\n## Step 3: Shared questions (ask once, reuse across all selected canvases)\nGo through these one at a time. For each:\n- Briefly explain what the section means (1-2 sentences)\n- Run tavily_search BEFORE suggesting anything\n- Give exactly 5 concrete, realistic examples relevant to the user's specific business. \n  Each example must be specific enough that the user could copy it directly into their \n  canvas without modification. Never give category labels like \"Human Resources\" or \n  \"Technology Infrastructure\" as examples \u2014 give the actual specific resource, partner, \n  channel, or activity. Bad example: \"Human Resources: skilled developers\" \u2014 Good example: \n  \"Mid-senior Laravel and React developers hired locally in Gda\u0144sk or remotely from Poland, \n  essential for delivering 2-week sprint cycles to long-term clients\" Format them as:\n  \u2022 Example 1: [specific answer] \u2014 [why this matters for their business]\n  \u2022 Example 2: [specific answer] \u2014 [why this matters for their business]\n  \u2022 Example 3: [specific answer] \u2014 [why this matters for their business]\n- Always include the 5 examples IN THE SAME MESSAGE as the question. \n  Never ask a question and wait for the user to request examples. \n  The format for every section question must be:\n  \n  [Brief explanation of what this section means]\n  \n  Based on my research, here are 5 examples relevant to your business:\n  \u2022 Example 1: [specific answer] \u2014 [why this matters]\n  \u2022 Example 2: [specific answer] \u2014 [why this matters]\n  \u2022 Example 3: [specific answer] \u2014 [why this matters]\n  \u2022 Example 4: [specific answer] \u2014 [why this matters]\n  \u2022 Example 5: [specific answer] \u2014 [why this matters]\n  \n  Which of these apply to your business? Feel free to confirm, combine, modify, or add your own.\n- If the user gives a one-word or one-line answer, always follow up with: \"Can you tell me more about that? For example: [specific follow-up question relevant to their business]\"\n- Never move to the next section until you have at least 2-3 solid, specific answers\n\nShared sections:\n1. Customer Segments \u2014 who exactly are your target customers?\n2. Value Proposition / Unique Value Proposition \u2014 what makes your offer unique compared to competitors?\n3. Channels \u2014 how do you reach and acquire your customers?\n4. Revenue Streams \u2014 how do you make money, and what does your pricing look like?\n5. Cost Structure \u2014 what are your main expenses? Break them into fixed and variable.\n\n## Step 4: Canvas-specific questions\nAsk these only if the relevant canvas was selected. Apply the same 3-example format and pushback rules as Step 3.\n\n### BMC specific:\n- Customer Relationships \u2014 IMPORTANT: this is NOT about how you find customers (that is Channels). This is about how you maintain and grow relationships AFTER the customer signs. Push for specifics like dedicated account managers, sprint reviews, SLA commitments, onboarding processes. If the user mentions contact forms or job boards here, correct them and explain the difference.\n- Key Resources \u2014 what physical, intellectual, human, and financial assets does your business depend on?\n- Key Activities \u2014 what are the core tasks that keep your business running and delivering value?\n- Key Partners \u2014 who do you work with or depend on? Name specific companies, platforms, or types of partners.\n\n### Lean Canvas specific:\n- Problem \u2014 what are the top 1-3 specific problems your customers face? Do not accept vague answers like \"they need software\" \u2014 push for the real pain point.\n- Solution \u2014 how does your product/service solve each of those problems specifically?\n- Key Metrics \u2014 demand minimum 3 metrics, ideally 5. If the user gives only one, say: \"That is a great start \u2014 what else do you track? Consider: client retention rate, average project value, time-to-delivery, referral rate, monthly recurring revenue.\" Never accept a single metric as complete.\n- Unfair Advantage \u2014 what do you have that competitors cannot easily copy? Push hard here \u2014 do not accept generic answers like \"good team\" or \"quality work.\" Ask: \"Why can't a competitor replicate that tomorrow?\"\n\n### SWOT specific:\n- Strengths \u2014 what does your business do better than competitors right now?\n- Weaknesses \u2014 what are your current limitations or gaps that hurt you?\n- Opportunities \u2014 what market trends or gaps could you exploit in the next 12 months?\n- Threats \u2014 what external factors could seriously harm your business?\n\nFor every single SWOT point the user gives, always ask: \"Why is that specifically a strength/weakness/opportunity/threat for your business right now?\" Never accept one-word answers like \"talent\" or \"competition\" \u2014 push for a full sentence explaining the business impact.\n\n### If All three selected:\n- Ask BMC-specific, Lean Canvas-specific, and SWOT-specific questions all\n- Reuse shared answers wherever sections overlap\n- Never ask the same question twice\n\n## Step 5: Competitive gap analysis\nAfter all questions are answered:\n- Use tavily_research to look up the user's named competitors plus any you found during discovery\n- For each competitor, find: what they offer, how they position themselves, and what they charge if possible\n- Identify specifically what the user's canvas does differently from each named competitor\n- Point out weak spots or missing opportunities in the user's canvas compared to competitors\n- Never write a generic positioning paragraph \u2014 always name specific competitors and specific differences\n\n## Step 6: Final output\nGenerate all selected canvases as clearly labeled separate sections. Each must be:\n- Fully filled with specific, non-generic content reflecting the actual conversation\n- Written in complete sentences, not just bullet labels\n- Include a brief explanation of why each answer matters for this specific business\n- Ask user for verbal confirmation that they are satisfied so you can say [CANVAS_READY] so that next steps can be execute\n\nThen add:\n- A \"Competitive Positioning\" paragraph that names real competitors and explains specifically how this business differs from each one\n- 3 concrete recommended next steps based on the canvases, specific to this business and its current stage\n\n## Research rules:\n- You have access to two search tools:\n  - tavily_search \u2014 use for quick lookups before suggesting answers for each section\n  - tavily_research \u2014 use for deeper research on competitors and market context\n- Always include the user's business type AND location in every search query\n- Required searches per section:\n  - Customer Segments: \"[business type] [country] target customer segments\"\n  - Value Proposition: \"[business type] [country] competitors value proposition\"\n  - Channels: \"[business type] [country] marketing channels customer acquisition\"\n  - Customer Relationships: \"[business type] [country] customer retention strategies\"\n  - Revenue Streams: \"[business type] [country] revenue models pricing\"\n  - Key Resources: \"[business type] [country] key resources tools platforms\"\n  - Key Activities: \"[business type] [country] core activities operations\"\n  - Key Partners: \"[business type] [country] typical partners suppliers\"\n  - Cost Structure: \"[business type] [country] operating costs salaries expenses\"\n  - Problem: \"[business type] [country] customer pain points problems\"\n  - Solution: \"[business type] [country] solutions innovations\"\n  - Key Metrics: \"[business type] [country] KPIs success metrics\"\n  - Unfair Advantage: \"[business type] [country] competitive advantages\"\n  - SWOT Opportunities: \"[business type] [country] market trends growth opportunities\"\n  - SWOT Threats: \"[business type] [country] market risks threats regulations\"\n- Always mention when a suggestion is based on search results\n- Never skip research \u2014 always search before suggesting answers for any section\n\n## General rules:\n- Never split a question and its examples into separate messages \u2014 always ask the question and provide examples together in one message\n- Never wait for the user to ask for examples \u2014 always provide them proactively\n- Ask one thing at a time \u2014 never overwhelm the user with multiple questions at once\n- Never accept vague answers \u2014 always push back with a specific follow-up question\n- Never move to the next section until the current one has at least 2-3 solid, specific answers\n- Never write generic content \u2014 every suggestion and every output line must reflect this specific business, their location, and their competitors\n- Keep your tone friendly, encouraging, and consultative \u2014 like a trusted advisor who genuinely wants the best outcome, not an interrogator\n- Always base suggestions on the user's specific business context and location\n- If the user seems unsure about a section, offer to research and suggest answers for them rather than leaving them stuck",
          "returnIntermediateSteps": false
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "2747e4b4-0b9c-4011-8711-7a2b3f037927",
      "name": "Canvas info parser",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        768,
        656
      ],
      "parameters": {
        "text": "=You are a data extraction engine. Below is the complete business strategy conversation between the AI consultant and the user. Extract everything the user provided into the exact structured JSON schema you've been given. Do not ask questions, do not add new content, and do not omit anything the user already said.\n\nRules:\n1. Only populate businessModelCanvas if the user selected BMC (or \"All three\"). Otherwise set businessModelCanvas.generated to false and leave its other fields empty.\n2. Only populate leanCanvas if the user selected Lean Canvas (or \"All three\"). Otherwise set leanCanvas.generated to false and leave its other fields empty.\n3. Only populate swot if the user selected SWOT Analysis (or \"All three\"). Otherwise set swot.generated to false and leave its other fields empty.\n4. Set meta.readyToGenerate to true only if the conversation reached a genuine end \u2014 all required sections for the selected canvas(es) were answered, the competitive gap analysis was discussed, and the final canvas summary was already presented to the user in chat (look for the [CANVAS_READY] marker as confirmation of this). If any required section is still missing or unconfirmed, set readyToGenerate to false and leave the rest of the structure empty or partial.\n5. Use the user's own words and confirmed examples \u2014 do not invent, generalize, or soften anything they said. Do not insert placeholder text like \"TBD\" or \"N/A\"; if something was genuinely never discussed, return an empty array for that field.\n6. Populate competitivePositioning with one entry per named competitor, summarizing exactly how the user's business differs from that competitor as already established in the conversation.\n7. Populate nextSteps with the exact next steps already given to the user, not new ones.\n8. Return only the structured JSON. Do not add commentary, explanations, or any text outside the schema.\n\nTake canvases from memory",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "e0e83a0e-6935-4330-832f-be20c7b667a6",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        912,
        880
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"required\": [\"meta\", \"businessModelCanvas\", \"leanCanvas\", \"swot\"],\n  \"properties\": {\n    \"meta\": {\n      \"type\": \"object\",\n      \"required\": [\"businessName\", \"country\", \"readyToGenerate\"],\n      \"properties\": {\n        \"businessName\": { \"type\": \"string\" },\n        \"country\": { \"type\": \"string\" },\n        \"readyToGenerate\": {\n          \"type\": \"boolean\",\n          \"description\": \"Must be true. Set true only when the conversation is fully finished and the user confirmed the output.\"\n        }\n      }\n    },\n    \"businessModelCanvas\": {\n      \"type\": \"object\",\n      \"required\": [\"generated\"],\n      \"properties\": {\n        \"generated\": { \"type\": \"boolean\", \"description\": \"True only if the user selected BMC.\" },\n        \"keyPartners\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"keyActivities\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"keyResources\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"valuePropositions\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"items\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n            \"tags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        },\n        \"customerRelationships\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"channels\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"customerSegments\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"items\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n            \"tags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        },\n        \"costStructure\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"fixed\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n            \"variable\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        },\n        \"revenueStreams\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"name\": { \"type\": \"string\" },\n              \"description\": { \"type\": \"string\" }\n            }\n          }\n        }\n      }\n    },\n    \"leanCanvas\": {\n      \"type\": \"object\",\n      \"required\": [\"generated\"],\n      \"properties\": {\n        \"generated\": { \"type\": \"boolean\", \"description\": \"True only if the user selected Lean Canvas.\" },\n        \"problem\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"items\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n            \"existingAlternatives\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        },\n        \"solution\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"keyMetrics\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"uniqueValueProposition\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"headline\": { \"type\": \"string\" },\n            \"concept\": { \"type\": \"string\" }\n          }\n        },\n        \"unfairAdvantage\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"channels\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"customerSegments\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"earlyAdopters\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n            \"targetMarket\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        },\n        \"costStructure\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"revenueStreams\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"name\": { \"type\": \"string\" },\n              \"description\": { \"type\": \"string\" }\n            }\n          }\n        }\n      }\n    },\n    \"swot\": {\n      \"type\": \"object\",\n      \"required\": [\"generated\"],\n      \"properties\": {\n        \"generated\": { \"type\": \"boolean\", \"description\": \"True only if the user selected SWOT Analysis.\" },\n        \"strengths\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"weaknesses\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"opportunities\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n        \"threats\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n      }\n    },\n    \"competitivePositioning\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"competitorName\": { \"type\": \"string\" },\n          \"differentiation\": { \"type\": \"string\" }\n        }\n      }\n    },\n    \"nextSteps\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" }\n    }\n  }\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "e2982428-b4c6-406d-8e07-223f6b4c86ef",
      "name": "Prepare Excel file from canvases",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        656
      ],
      "parameters": {
        "jsCode": "// n8n Code node \u2014 \"Run Once for All Items\"\n// ZERO external dependencies \u2014 uses only Node's built-in `zlib` module.\n// Safe for marketplace publishing: no npm installs, no NODE_FUNCTION_ALLOW_EXTERNAL needed.\n// Input:  item.json matching canvas-schema.json (output of Structured Output Parser)\n// Output: item.binary.data \u2014 xlsx file buffer\n\n// ============================================================================\n// MINIMAL ZIP WRITER (store only \u2014 zero require() calls, zero dependencies,\n// not even Node built-ins. Files are uncompressed, so the .xlsx is a bit\n// larger than an exceljs/zlib version, but it opens in Excel/Sheets/LibreOffice\n// exactly the same way.)\n// ============================================================================\n\nfunction crc32(buf) {\n  let c;\n  const table = crc32.table || (crc32.table = (() => {\n    const t = new Uint32Array(256);\n    for (let n = 0; n < 256; n++) {\n      c = n;\n      for (let k = 0; k < 8; k++) {\n        c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;\n      }\n      t[n] = c >>> 0;\n    }\n    return t;\n  })());\n  let crc = 0xffffffff;\n  for (let i = 0; i < buf.length; i++) {\n    crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);\n  }\n  return (crc ^ 0xffffffff) >>> 0;\n}\n\nfunction dosDateTime(date) {\n  const d = date || new Date();\n  const dosTime =\n    ((d.getHours() & 0x1f) << 11) |\n    ((d.getMinutes() & 0x3f) << 5) |\n    ((Math.floor(d.getSeconds() / 2)) & 0x1f);\n  const dosDate =\n    (((d.getFullYear() - 1980) & 0x7f) << 9) |\n    (((d.getMonth() + 1) & 0xf) << 5) |\n    (d.getDate() & 0x1f);\n  return { dosTime, dosDate };\n}\n\nfunction makeZip(files) {\n  // files: [{ name: string, data: Buffer }]\n  const localChunks = [];\n  const centralChunks = [];\n  let offset = 0;\n  const { dosTime, dosDate } = dosDateTime();\n\n  for (const file of files) {\n    const nameBuf = Buffer.from(file.name, 'utf8');\n    const rawData = file.data;\n    const crc = crc32(rawData);\n    const dataToStore = rawData; // stored, uncompressed \u2014 no zlib needed\n    const method = 0;\n\n    // Local file header\n    const localHeader = Buffer.alloc(30);\n    localHeader.writeUInt32LE(0x04034b50, 0);   // local file header signature\n    localHeader.writeUInt16LE(20, 4);           // version needed\n    localHeader.writeUInt16LE(0, 6);            // flags\n    localHeader.writeUInt16LE(method, 8);       // compression method\n    localHeader.writeUInt16LE(dosTime, 10);\n    localHeader.writeUInt16LE(dosDate, 12);\n    localHeader.writeUInt32LE(crc, 14);\n    localHeader.writeUInt32LE(dataToStore.length, 18); // compressed size\n    localHeader.writeUInt32LE(rawData.length, 22);     // uncompressed size\n    localHeader.writeUInt16LE(nameBuf.length, 26);\n    localHeader.writeUInt16LE(0, 28);           // extra field length\n\n    localChunks.push(localHeader, nameBuf, dataToStore);\n\n    // Central directory header\n    const centralHeader = Buffer.alloc(46);\n    centralHeader.writeUInt32LE(0x02014b50, 0);  // central dir signature\n    centralHeader.writeUInt16LE(20, 4);          // version made by\n    centralHeader.writeUInt16LE(20, 6);          // version needed\n    centralHeader.writeUInt16LE(0, 8);           // flags\n    centralHeader.writeUInt16LE(method, 10);\n    centralHeader.writeUInt16LE(dosTime, 12);\n    centralHeader.writeUInt16LE(dosDate, 14);\n    centralHeader.writeUInt32LE(crc, 16);\n    centralHeader.writeUInt32LE(dataToStore.length, 20);\n    centralHeader.writeUInt32LE(rawData.length, 24);\n    centralHeader.writeUInt16LE(nameBuf.length, 28);\n    centralHeader.writeUInt16LE(0, 30);          // extra field length\n    centralHeader.writeUInt16LE(0, 32);          // comment length\n    centralHeader.writeUInt16LE(0, 34);          // disk number start\n    centralHeader.writeUInt16LE(0, 36);          // internal attrs\n    centralHeader.writeUInt32LE(0, 38);          // external attrs\n    centralHeader.writeUInt32LE(offset, 42);     // relative offset of local header\n\n    centralChunks.push(centralHeader, nameBuf);\n\n    offset += localHeader.length + nameBuf.length + dataToStore.length;\n  }\n\n  const centralDirStart = offset;\n  const centralDirBuf = Buffer.concat(centralChunks);\n  const centralDirSize = centralDirBuf.length;\n\n  const eocd = Buffer.alloc(22);\n  eocd.writeUInt32LE(0x06054b50, 0);        // EOCD signature\n  eocd.writeUInt16LE(0, 4);                 // disk number\n  eocd.writeUInt16LE(0, 6);                 // disk with central dir\n  eocd.writeUInt16LE(files.length, 8);      // entries on this disk\n  eocd.writeUInt16LE(files.length, 10);     // total entries\n  eocd.writeUInt32LE(centralDirSize, 12);\n  eocd.writeUInt32LE(centralDirStart, 16);\n  eocd.writeUInt16LE(0, 20);                // comment length\n\n  return Buffer.concat([...localChunks, centralDirBuf, eocd]);\n}\n\n// ============================================================================\n// XML HELPERS\n// ============================================================================\n\nfunction xmlEscape(str) {\n  return String(str ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&apos;');\n}\n\nfunction colLetter(n) {\n  let s = '';\n  while (n > 0) {\n    const m = (n - 1) % 26;\n    s = String.fromCharCode(65 + m) + s;\n    n = Math.floor((n - m) / 26);\n  }\n  return s;\n}\n\nfunction cellRef(col, row) {\n  return `${colLetter(col)}${row}`;\n}\n\n// ============================================================================\n// STYLE REGISTRY (fonts, fills, borders, alignment -> single style index)\n// ============================================================================\n\nfunction createStyleRegistry() {\n  const fonts = [{ name: 'Arial', size: 10 }]; // index 0 = default\n  const fills = [{ type: 'none' }, { type: 'gray125' }]; // 0,1 reserved by spec\n  const borders = [{ none: true }]; // index 0 = no border\n  const xfs = [{ fontId: 0, fillId: 0, borderId: 0, align: null, numFmtId: 0 }]; // index 0 = default\n\n  const cache = new Map();\n\n  function addFont(font) {\n    const key = JSON.stringify(font);\n    let idx = fonts.findIndex((f) => JSON.stringify(f) === key);\n    if (idx === -1) {\n      fonts.push(font);\n      idx = fonts.length - 1;\n    }\n    return idx;\n  }\n\n  function addFill(argb) {\n    if (!argb) return 0;\n    const key = JSON.stringify({ argb });\n    let idx = fills.findIndex((f) => JSON.stringify(f) === key);\n    if (idx === -1) {\n      fills.push({ argb });\n      idx = fills.length - 1;\n    }\n    return idx;\n  }\n\n  function addBorder(hasBorder) {\n    if (!hasBorder) return 0;\n    const key = JSON.stringify({ thin: true });\n    let idx = borders.findIndex((b) => JSON.stringify(b) === key);\n    if (idx === -1) {\n      borders.push({ thin: true });\n      idx = borders.length - 1;\n    }\n    return idx;\n  }\n\n  // opts: { bold, size, fontColor, fillColor, border, wrap, valign, halign, indent }\n  function getStyle(opts = {}) {\n    const key = JSON.stringify(opts);\n    if (cache.has(key)) return cache.get(key);\n\n    const fontId = addFont({\n      name: 'Arial',\n      size: opts.size || 10,\n      bold: !!opts.bold,\n      color: opts.fontColor || 'FF000000',\n    });\n    const fillId = addFill(opts.fillColor);\n    const borderId = addBorder(opts.border !== false); // default true (thin border)\n\n    const xf = {\n      fontId,\n      fillId,\n      borderId,\n      align: {\n        vertical: opts.valign || 'top',\n        horizontal: opts.halign || 'left',\n        wrapText: opts.wrap !== false,\n        indent: opts.indent || 0,\n      },\n      numFmtId: 0,\n    };\n    xfs.push(xf);\n    const idx = xfs.length - 1;\n    cache.set(key, idx);\n    return idx;\n  }\n\n  function buildStylesXml() {\n    const fontsXml = fonts.map((f) => {\n      if (!f.bold && !f.color && f.size === 10 && f.name === 'Arial' && fonts.indexOf(f) === 0) {\n        return `<font><sz val=\"10\"/><name val=\"Arial\"/></font>`;\n      }\n      return `<font>${f.bold ? '<b/>' : ''}<sz val=\"${f.size || 10}\"/><color rgb=\"${f.color || 'FF000000'}\"/><name val=\"${f.name || 'Arial'}\"/></font>`;\n    }).join('');\n\n    const fillsXml = fills.map((f, i) => {\n      if (i === 0) return `<fill><patternFill patternType=\"none\"/></fill>`;\n      if (i === 1) return `<fill><patternFill patternType=\"gray125\"/></fill>`;\n      return `<fill><patternFill patternType=\"solid\"><fgColor rgb=\"${f.argb}\"/><bgColor rgb=\"${f.argb}\"/></patternFill></fill>`;\n    }).join('');\n\n    const borderThin = '<color rgb=\"FFE0DFD8\"/>';\n    const bordersXml = borders.map((b, i) => {\n      if (i === 0 || b.none) {\n        return `<border><left/><right/><top/><bottom/><diagonal/></border>`;\n      }\n      return `<border><left style=\"thin\">${borderThin}</left><right style=\"thin\">${borderThin}</right><top style=\"thin\">${borderThin}</top><bottom style=\"thin\">${borderThin}</bottom><diagonal/></border>`;\n    }).join('');\n\n    const xfsXml = xfs.map((xf) => {\n      const align = xf.align\n        ? `<alignment vertical=\"${xf.align.vertical}\" horizontal=\"${xf.align.horizontal}\" wrapText=\"${xf.align.wrapText ? 1 : 0}\"${xf.align.indent ? ` indent=\"${xf.align.indent}\"` : ''}/>`\n        : '';\n      return `<xf numFmtId=\"${xf.numFmtId}\" fontId=\"${xf.fontId}\" fillId=\"${xf.fillId}\" borderId=\"${xf.borderId}\" xfId=\"0\" applyFont=\"1\" applyFill=\"1\" applyBorder=\"1\" applyAlignment=\"1\">${align}</xf>`;\n    }).join('');\n\n    return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\n<fonts count=\"${fonts.length}\">${fontsXml}</fonts>\n<fills count=\"${fills.length}\">${fillsXml}</fills>\n<borders count=\"${borders.length}\">${bordersXml}</borders>\n<cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs>\n<cellXfs count=\"${xfs.length}\">${xfsXml}</cellXfs>\n<cellStyles count=\"1\"><cellStyle name=\"Normal\" xfId=\"0\" builtinId=\"0\"/></cellStyles>\n</styleSheet>`;\n  }\n\n  return { getStyle, buildStylesXml };\n}\n\n// ============================================================================\n// WORKSHEET BUILDER\n// ============================================================================\n\nfunction createWorksheet(name) {\n  const cells = new Map(); // \"col,row\" -> { value, styleId, isNumber }\n  const merges = [];\n  const colWidths = new Map();\n  let rowHeightsMap = new Map();\n  let maxCol = 1;\n  let maxRow = 1;\n  let showGridLines = true;\n\n  function setCell(col, row, value, styleId) {\n    maxCol = Math.max(maxCol, col);\n    maxRow = Math.max(maxRow, row);\n    cells.set(`${col},${row}`, { value, styleId: styleId || 0 });\n  }\n\n  function setColWidth(col, width) {\n    colWidths.set(col, width);\n  }\n\n  function setRowHeight(row, height) {\n    rowHeightsMap.set(row, height);\n  }\n\n  // Merge a rectangular range and stamp every cell in it with `styleId`.\n  // Only the top-left cell carries `value`.\n  function mergeBlock({ col1, col2, row1, row2, value, styleId }) {\n    merges.push(`${cellRef(col1, row1)}:${cellRef(col2, row2)}`);\n    for (let r = row1; r <= row2; r++) {\n      for (let c = col1; c <= col2; c++) {\n        const isTopLeft = r === row1 && c === col1;\n        setCell(c, r, isTopLeft ? value : undefined, styleId);\n      }\n    }\n  }\n\n  function setShowGridLines(val) {\n    showGridLines = val;\n  }\n\n  function buildSheetXml() {\n    // group cells by row\n    const rows = new Map();\n    for (const [key, cellData] of cells.entries()) {\n      const [c, r] = key.split(',').map(Number);\n      if (!rows.has(r)) rows.set(r, []);\n      rows.get(r).push({ col: c, ...cellData });\n    }\n\n    const sortedRowNums = Array.from(rows.keys()).sort((a, b) => a - b);\n\n    const rowsXml = sortedRowNums.map((rNum) => {\n      const rowCells = rows.get(rNum).sort((a, b) => a.col - b.col);\n      const cellsXml = rowCells.map((cd) => {\n        const ref = cellRef(cd.col, rNum);\n        const sAttr = cd.styleId ? ` s=\"${cd.styleId}\"` : '';\n        if (cd.value === undefined || cd.value === null || cd.value === '') {\n          return `<c r=\"${ref}\"${sAttr}/>`;\n        }\n        return `<c r=\"${ref}\"${sAttr} t=\"inlineStr\"><is><t xml:space=\"preserve\">${xmlEscape(cd.value)}</t></is></c>`;\n      }).join('');\n      const heightAttr = rowHeightsMap.has(rNum) ? ` ht=\"${rowHeightsMap.get(rNum)}\" customHeight=\"1\"` : '';\n      return `<row r=\"${rNum}\"${heightAttr}>${cellsXml}</row>`;\n    }).join('');\n\n    const colsXml = colWidths.size\n      ? `<cols>${Array.from(colWidths.entries()).map(([c, w]) => `<col min=\"${c}\" max=\"${c}\" width=\"${w}\" customWidth=\"1\"/>`).join('')}</cols>`\n      : '';\n\n    const mergesXml = merges.length\n      ? `<mergeCells count=\"${merges.length}\">${merges.map((m) => `<mergeCell ref=\"${m}\"/>`).join('')}</mergeCells>`\n      : '';\n\n    return `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\n<sheetViews><sheetView showGridLines=\"${showGridLines ? 1 : 0}\" workbookViewId=\"0\"/></sheetViews>\n<sheetFormatPr defaultRowHeight=\"15\"/>\n${colsXml}\n<sheetData>${rowsXml}</sheetData>\n${mergesXml}\n</worksheet>`;\n  }\n\n  return { name, setCell, setColWidth, setRowHeight, mergeBlock, setShowGridLines, buildSheetXml };\n}\n\n// ============================================================================\n// WORKBOOK\n// ============================================================================\n\nfunction buildWorkbookFiles(worksheets) {\n  const files = [];\n\n  files.push({\n    name: '[Content_Types].xml',\n    data: Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n<Default Extension=\"xml\" ContentType=\"application/xml\"/>\n<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>\n<Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>\n${worksheets.map((_, i) => `<Override PartName=\"/xl/worksheets/sheet${i + 1}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>`).join('\\n')}\n</Types>`, 'utf8'),\n  });\n\n  files.push({\n    name: '_rels/.rels',\n    data: Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>\n</Relationships>`, 'utf8'),\n  });\n\n  files.push({\n    name: 'xl/workbook.xml',\n    data: Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\n<sheets>${worksheets.map((ws, i) => `<sheet name=\"${xmlEscape(ws.name)}\" sheetId=\"${i + 1}\" r:id=\"rId${i + 1}\"/>`).join('')}</sheets>\n</workbook>`, 'utf8'),\n  });\n\n  files.push({\n    name: 'xl/_rels/workbook.xml.rels',\n    data: Buffer.from(`<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n${worksheets.map((_, i) => `<Relationship Id=\"rId${i + 1}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet${i + 1}.xml\"/>`).join('\\n')}\n<Relationship Id=\"rId${worksheets.length + 1}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>\n</Relationships>`, 'utf8'),\n  });\n\n  worksheets.forEach((ws, i) => {\n    files.push({\n      name: `xl/worksheets/sheet${i + 1}.xml`,\n      data: Buffer.from(ws.buildSheetXml(), 'utf8'),\n    });\n  });\n\n  return files;\n}\n\n// ============================================================================\n// CANVAS-SPECIFIC BUILD LOGIC (same layout as the original exceljs version)\n// ============================================================================\n\nconst styleReg = createStyleRegistry();\n\nconst S = {\n  title: styleReg.getStyle({ bold: true, size: 14, fontColor: 'FFFFFFFF', fillColor: 'FF1A1A18', valign: 'center', border: false, indent: 1, wrap: false }),\n  label: styleReg.getStyle({ bold: true, size: 9, fontColor: 'FF6B6B67', fillColor: 'FFF1EFE8' }),\n  body: styleReg.getStyle({ fontColor: 'FF1A1A18' }),\n  swotHeader: styleReg.getStyle({ bold: true, size: 9, fontColor: 'FF6B6B67', halign: 'center', border: false, wrap: false }),\n  swotS: styleReg.getStyle({ fontColor: 'FF085041', fillColor: 'FFE1F5EE' }),\n  swotW: styleReg.getStyle({ fontColor: 'FF712B13', fillColor: 'FFFAECE7' }),\n  swotO: styleReg.getStyle({ fontColor: 'FF0C447C', fillColor: 'FFE6F1FB' }),\n  swotT: styleReg.getStyle({ fontColor: 'FF633806', fillColor: 'FFFAEEDA' }),\n};\n\nfunction bulletText(lines) {\n  if (Array.isArray(lines)) return lines.filter(Boolean).map((l) => `\u2022 ${l}`).join('\\n');\n  return lines || '';\n}\n\nfunction revenueText(streams) {\n  if (!streams || !streams.length) return '';\n  return streams.map((s) => `${s.name}: ${s.description}`).join('\\n');\n}\n\nfunction block(ws, { col1, col2, rowStart, rowEnd, label, content, bodyStyle }) {\n  ws.mergeBlock({ col1, col2, row1: rowStart, row2: rowStart, value: label, styleId: S.label });\n  ws.mergeBlock({ col1, col2, row1: rowStart + 1, row2: rowEnd, value: bulletText(content), styleId: bodyStyle || S.body });\n}\n\nfunction buildWorkbookBuffer(data) {\n  const bmc = data.businessModelCanvas || {};\n  const lc = data.leanCanvas || {};\n  const sw = data.swot || {};\n  const meta = data.meta || {};\n  const worksheets = [];\n\n  // ====================== BMC SHEET ======================\n  if (bmc.generated) {\n    const ws = createWorksheet('Business Model Canvas');\n    ws.setShowGridLines(false);\n    for (let c = 1; c <= 10; c++) ws.setColWidth(c, 16);\n\n    ws.mergeBlock({ col1: 1, col2: 10, row1: 1, row2: 1, value: `Business Model Canvas \u2014 ${meta.businessName || ''}`, styleId: S.title });\n    ws.setRowHeight(1, 26);\n\n    block(ws, { col1: 1, col2: 2, rowStart: 3, rowEnd: 10, label: 'Key Partners', content: bmc.keyPartners });\n    block(ws, { col1: 3, col2: 4, rowStart: 3, rowEnd: 6, label: 'Key Activities', content: bmc.keyActivities });\n    block(ws, { col1: 3, col2: 4, rowStart: 7, rowEnd: 10, label: 'Key Resources', content: bmc.keyResources });\n    block(ws, {\n      col1: 5, col2: 6, rowStart: 3, rowEnd: 10, label: 'Value Propositions',\n      content: [...(bmc.valuePropositions?.items || []), ...(bmc.valuePropositions?.tags || [])],\n    });\n    block(ws, { col1: 7, col2: 8, rowStart: 3, rowEnd: 6, label: 'Customer Relationships', content: bmc.customerRelationships });\n    block(ws, { col1: 7, col2: 8, rowStart: 7, rowEnd: 10, label: 'Channels', content: bmc.channels });\n    block(ws, {\n      col1: 9, col2: 10, rowStart: 3, rowEnd: 10, label: 'Customer Segments',\n      content: [...(bmc.customerSegments?.items || []), ...(bmc.customerSegments?.tags || [])],\n    });\n\n    block(ws, { col1: 1, col2: 5, rowStart: 11, rowEnd: 14, label: 'Cost Structure \u2014 Fixed', content: bmc.costStructure?.fixed });\n    block(ws, { col1: 6, col2: 10, rowStart: 11, rowEnd: 14, label: 'Cost Structure \u2014 Variable', content: bmc.costStructure?.variable });\n    block(ws, { col1: 1, col2: 10, rowStart: 15, rowEnd: 18, label: 'Revenue Streams', content: revenueText(bmc.revenueStreams) });\n\n    worksheets.push(ws);\n  }\n\n  // ====================== LEAN CANVAS SHEET ======================\n  if (lc.generated) {\n    const ws = createWorksheet('Lean Canvas');\n    ws.setShowGridLines(false);\n    for (let c = 1; c <= 10; c++) ws.setColWidth(c, 16);\n\n    ws.mergeBlock({ col1: 1, col2: 10, row1: 1, row2: 1, value: `Lean Canvas \u2014 ${meta.businessName || ''}`, styleId: S.title });\n    ws.setRowHeight(1, 26);\n\n    block(ws, {\n      col1: 1, col2: 2, rowStart: 3, rowEnd: 10, label: 'Problem',\n      content: [...(lc.problem?.items || []), lc.problem?.existingAlternatives?.length ? '\u2014 Existing alternatives \u2014' : null, ...(lc.problem?.existingAlternatives || [])].filter(Boolean),\n    });\n    block(ws, { col1: 3, col2: 4, rowStart: 3, rowEnd: 6, label: 'Solution', content: lc.solution });\n    block(ws, { col1: 3, col2: 4, rowStart: 7, rowEnd: 10, label: 'Key Metrics', content: lc.keyMetrics });\n    block(ws, {\n      col1: 5, col2: 6, rowStart: 3, rowEnd: 10, label: 'Unique Value Proposition',\n      content: [lc.uniqueValueProposition?.headline, lc.uniqueValueProposition?.concept].filter(Boolean),\n    });\n    block(ws, { col1: 7, col2: 8, rowStart: 3, rowEnd: 6, label: 'Unfair Advantage', content: lc.unfairAdvantage });\n    block(ws, { col1: 7, col2: 8, rowStart: 7, rowEnd: 10, label: 'Channels', content: lc.channels });\n    block(ws, { col1: 9, col2: 10, rowStart: 3, rowEnd: 6, label: 'Early Adopters', content: lc.customerSegments?.earlyAdopters });\n    block(ws, { col1: 9, col2: 10, rowStart: 7, rowEnd: 10, label: 'Target Market', content: lc.customerSegments?.targetMarket });\n\n    block(ws, { col1: 1, col2: 5, rowStart: 11, rowEnd: 14, label: 'Cost Structure', content: lc.costStructure });\n    block(ws, { col1: 6, col2: 10, rowStart: 11, rowEnd: 14, label: 'Revenue Streams', content: revenueText(lc.revenueStreams) });\n\n    worksheets.push(ws);\n  }\n\n  // ====================== SWOT SHEET ======================\n  if (sw.generated) {\n    const ws = createWorksheet('SWOT Analysis');\n    ws.setShowGridLines(false);\n    for (let c = 1; c <= 8; c++) ws.setColWidth(c, 18);\n\n    ws.mergeBlock({ col1: 1, col2: 8, row1: 1, row2: 1, value: `SWOT Analysis \u2014 ${meta.businessName || ''}`, styleId: S.title });\n    ws.setRowHeight(1, 26);\n\n    ws.mergeBlock({ col1: 1, col2: 4, row1: 2, row2: 2, value: 'HELPFUL', styleId: S.swotHeader });\n    ws.mergeBlock({ col1: 5, col2: 8, row1: 2, row2: 2, value: 'HARMFUL', styleId: S.swotHeader });\n\n    block(ws, { col1: 1, col2: 4, rowStart: 3, rowEnd: 10, label: 'Strengths', content: sw.strengths, bodyStyle: S.swotS });\n    block(ws, { col1: 5, col2: 8, rowStart: 3, rowEnd: 10, label: 'Weaknesses', content: sw.weaknesses, bodyStyle: S.swotW });\n    block(ws, { col1: 1, col2: 4, rowStart: 11, rowEnd: 18, label: 'Opportunities', content: sw.opportunities, bodyStyle: S.swotO });\n    block(ws, { col1: 5, col2: 8, rowStart: 11, rowEnd: 18, label: 'Threats', content: sw.threats, bodyStyle: S.swotT });\n\n    worksheets.push(ws);\n  }\n\n  // ====================== POSITIONING + NEXT STEPS SHEET ======================\n  if (data.competitivePositioning?.length || data.nextSteps?.length) {\n    const ws = createWorksheet('Positioning & Next Steps');\n    ws.setShowGridLines(false);\n    ws.setColWidth(1, 28);\n    ws.setColWidth(2, 70);\n\n    ws.mergeBlock({ col1: 1, col2: 2, row1: 1, row2: 1, value: 'Competitive Positioning', styleId: S.title });\n    ws.setRowHeight(1, 24);\n\n    ws.setCell(1, 2, 'Competitor', S.label);\n    ws.setCell(2, 2, 'How We Differ', S.label);\n\n    let r = 3;\n    (data.competitivePositioning || []).forEach((row) => {\n      ws.setCell(1, r, row.competitorName, S.body);\n      ws.setCell(2, r, row.differentiation, S.body);\n      r++;\n    });\n\n    r += 1;\n    ws.mergeBlock({ col1: 1, col2: 2, row1: r, row2: r, value: 'Recommended Next Steps', styleId: S.title });\n    r++;\n    (data.nextSteps || []).forEach((step, i) => {\n      ws.mergeBlock({ col1: 1, col2: 2, row1: r, row2: r, value: `${i + 1}. ${step}`, styleId: S.body });\n      r++;\n    });\n\n    worksheets.push(ws);\n  }\n\n  const files = buildWorkbookFiles(worksheets);\n  files.push({ name: 'xl/styles.xml', data: Buffer.from(styleReg.buildStylesXml(), 'utf8') });\n\n  return makeZip(files);\n}\n\n// ============================================================================\n// n8n ENTRY POINT\n// ============================================================================\n\nconst raw = $input.first().json;\n\n// Basic LLM Chain (and some Agent configurations) wrap the parsed structured\n// output inside a key like `output`, `response`, or `text` (sometimes as a\n// JSON string) instead of returning it flat. Unwrap whichever shape shows up\n// so this script works regardless of which node type produced it.\nfunction unwrapCanvasData(input) {\n  const candidates = [\n    input,\n    input.output,\n    input.response,\n    input.response?.output,\n    input.json,\n  ];\n\n  for (const candidate of candidates) {\n    if (candidate && typeof candidate === 'object' && ('businessModelCanvas' in candidate || 'meta' in candidate)) {\n      return candidate;\n    }\n  }\n\n  // Sometimes the parsed object arrives as a JSON string instead of an object.\n  for (const key of ['output', 'text', 'response']) {\n    if (typeof input[key] === 'string') {\n      try {\n        const parsed = JSON.parse(input[key]);\n        if (parsed && (parsed.businessModelCanvas || parsed.meta)) return parsed;\n      } catch (e) { /* not JSON, ignore */ }\n    }\n  }\n\n  return null; // couldn't find recognizable canvas data anywhere\n}\n\nconst data = unwrapCanvasData(raw);\nconst meta = data?.meta || {};\n\nlet zipBuffer;\nif (data) {\n  zipBuffer = buildWorkbookBuffer(data);\n} else {\n  // Nothing recognizable was found \u2014 instead of producing a silently empty\n  // file, dump the raw input into a debug sheet so you can see exactly what\n  // this node actually received and fix the upstream field path.\n  const ws = createWorksheet('Debug \u2014 Raw Input');\n  ws.setShowGridLines(true);\n  ws.setColWidth(1, 100);\n  ws.mergeBlock({\n    col1: 1, col2: 1, row1: 1, row2: 1,\n    value: 'No businessModelCanvas/meta found in input. Raw JSON below:',\n    styleId: S.label,\n  });\n  const jsonText = JSON.stringify(raw, null, 2);\n  const lines = jsonText.split('\\n');\n  lines.forEach((line, i) => {\n    ws.setCell(1, i + 2, line, S.body);\n  });\n  const files = buildWorkbookFiles([ws]);\n  files.push({ name: 'xl/styles.xml', data: Buffer.from(styleReg.buildStylesXml(), 'utf8') });\n  zipBuffer = makeZip(files);\n}\n\nreturn [\n  {\n    json: {\n      businessName: meta.businessName || 'business',\n      dataFound: !!data,\n    },\n    binary: {\n      data: {\n        data: zipBuffer.toString('base64'),\n        mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n        fileName: `${(meta.businessName || 'business-canvas').replace(/[^a-z0-9]+/gi, '-')}.xlsx`,\n      },\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "1b30874c-4af6-4201-9809-b00e045e0953",
      "name": "Notify user about sent email",
      "type": "@n8n/n8n-nodes-langchain.chat",
      "position": [
        1568,
        656
      ],
      "parameters": {
        "message": "=Check your inbox!",
        "options": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "7d5852ea-5274-4011-8122-9631858d680b",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        576,
        880
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {
          "maxRetries": 15
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "483dd627-4e6a-49f5-91a7-00056adb934e",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        304
      ],
      "parameters": {
        "width": 416,
        "height": 736,
        "content": "## Business Model Canvas Generator\n\n### How it works\n\nConducts a conversational AI session to guide users through business strategy design. Collects responses across Business Model Canvas, Lean Canvas, and/or SWOT Analysis dimensions with mandatory web research backing each section. Generates structured output and delivers it as a branded Excel file.\n\n### Setup steps\n\n- [ ] Configure OpenAI credentials (GPT-4o) for the Canvas generator and Canvas info parser agents\n- [ ] Set up Tavily API key for web research (basic and advanced depth)\n- [ ] Connect MongoDB account for chat memory (database: N8N_Business_model_canvas_generator)\n- [ ] Configure Gmail credentials for output delivery\n- [ ] Test webhook IDs on chat trigger and response nodes\n\n\nNeed help? Contact us [here](https://sailingbyte.com/contact/) or visit [sailingbyte.com](https://sailingbyte.com)!\n\nHappy hacking!"
      },
      "typeVersion": 1
    },
    {
      "id": "d512caba-b7d0-4599-b89a-1a95ce0aeb6f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -272,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 560,
        "content": "## User conversation flow\n\nReceives canvas type selection and business details via chat. Routes responses through a memory-backed AI agent that enforces depth and specificity via system prompt."
      },
      "typeVersion": 1
    },
    {
      "id": "89200114-b469-4ea5-a122-ff253ff63229",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 560,
        "content": "## Structured data parsing\n\nExtracts confirmed canvas data from full conversation history. Basic LLM Chain (not Agent) enforces schema via Structured Output Parser to avoid iteration exhaustion. Produces valid JSON matching canvas-schema."
      },
      "typeVersion": 1
    },
    {
      "id": "853316c8-dbbe-475e-9a8b-8f8091ccffd5",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 560,
        "content": "## Canvas delivery\n\nBuilds multi-sheet Excel workbook from parsed data. One sheet per canvas type (BMC, Lean, SWOT). Includes Positioning & Next Steps sheet. Delivers via branded email + in-chat confirmation."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Send a message": {
      "main": [
        [
          {
            "node": "Notify user about sent email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "tavily_search1": {
      "ai_tool": [
        [
          {
            "node": "Canvas generator",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Canvas generator": {
      "main": [
        [
          {
            "node": "Generate canvases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "tavily_research1": {
      "ai_tool": [
        [
          {
            "node": "Canvas generator",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Generate canvases": {
      "main": [
        [
          {
            "node": "Canvas info parser",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Canvas generator",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Canvas info parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Canvas info parser": {
      "main": [
        [
          {
            "node": "Prepare Excel file from canvases",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MongoDB Chat Memory1": {
      "ai_memory": [
        [
          {
            "node": "Canvas generator",
            "type": "ai_memory",
            "index": 0
          },
          {
            "node": "Canvas info parser",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Canvas info parser",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Canvas generator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Excel file from canvases": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}