AutomationFlowsAI & RAG › Generate Agile Project Plan Pdfs From Proposals with Claude and Sharepoint

Generate Agile Project Plan Pdfs From Proposals with Claude and Sharepoint

ByAI Solutions @legalgpts on n8n.io

This workflow receives a project outline PDF via webhook, extracts its text, uses Anthropic Claude to generate a structured project plan, renders it into an HTML report, converts it to PDF via an HTTP service, uploads it to Microsoft SharePoint, and sends an Outlook email…

Webhook trigger★★★★☆ complexityAI-powered17 nodesChain LlmMicrosoft SharePointAnthropic ChatHTTP RequestMicrosoft Outlook
AI & RAG Trigger: Webhook Nodes: 17 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #18075 — we link there as the canonical source.

This workflow follows the Chainllm → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "XBas4tC7AwsItnwO",
  "name": "N8N-Project Proposal",
  "tags": [],
  "nodes": [
    {
      "id": "92bcfe65-0031-4217-96a4-82776577977d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        -176
      ],
      "parameters": {
        "width": 480,
        "height": 832,
        "content": "## N8N-Project Proposal\n\n### How it works\n\nThis workflow receives a posted project plan PDF, extracts its text, and sends the content through an AI agent to generate structured project plan insights. It parses the AI output, builds an HTML report, converts that report into a PDF using an external service, uploads the PDF to SharePoint, and sends an Outlook email notification.\n\n### Setup steps\n\n- Configure the webhook endpoint and ensure the posting system sends the project plan PDF in the expected binary field.\n- Add Anthropic/Claude credentials for the LangChain chat model used by the AI agent.\n- Update the HTML-to-PDF HTTP Request node with the real service URL, authentication, and expected request/response format.\n- Connect Microsoft SharePoint credentials and configure the target site, drive, folder, and filename settings.\n- Connect Microsoft Outlook credentials and configure notification recipients, sender options, and email content fields.\n\n### Customization\n\nCustomize the AI prompt and parsing code to match your proposal format, adjust the HTML template and styling for branded reports, and change the SharePoint destination or Outlook recipients for different teams or projects."
      },
      "typeVersion": 1
    },
    {
      "id": "b8143349-395a-4ba6-8bbc-1258c2f0590b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 576,
        "height": 336,
        "content": "## Receive and prepare PDF\n\nAccepts an uploaded project plan through the webhook, extracts text from the PDF, and packages the extracted content into the structure needed for AI processing. These nodes form the leftmost input/preprocessing cluster."
      },
      "typeVersion": 1
    },
    {
      "id": "7430d573-f683-4bcc-aa76-29138b095baf",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 496,
        "content": "## Generate and parse plan\n\nUses the Claude-backed LangChain agent to generate project plan insights from the extracted PDF data, then parses the AI response into usable structured plan data. The Claude model subnode is grouped with its parent AI chain because it directly powers the generation step and is spatially nested nearby."
      },
      "typeVersion": 1
    },
    {
      "id": "43f57938-5e60-4ee4-b7e0-ef06b61b8343",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 336,
        "content": "## Build PDF report\n\nTransforms the parsed project plan into an HTML report, prepares the HTML payload, and sends it to an external HTML-to-PDF service. This middle-right cluster handles report rendering and PDF generation."
      },
      "typeVersion": 1
    },
    {
      "id": "4c86e1c7-76f2-4e52-a405-6fe486ebe4f9",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1856,
        -144
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 336,
        "content": "## Store and notify\n\nUploads the generated PDF to SharePoint, creates the notification email content, and sends the final Outlook notification. These nodes are positioned together at the far right as the workflow\u2019s delivery and notification cluster."
      },
      "typeVersion": 1
    },
    {
      "id": "61fc632e-04ce-44b2-997c-dab60a5db684",
      "name": "When Project Plan Uploaded",
      "type": "n8n-nodes-base.webhook",
      "position": [
        16,
        32
      ],
      "parameters": {
        "path": "project-plan-generator",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "dfe1411f-e5b1-43f0-bb01-d2c59bab590b",
      "name": "Project Plan Generator Agent",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        688,
        32
      ],
      "parameters": {
        "text": "==Project Outline Document (extracted text):\n{{ $json.pdfText }}\n\nConsultation Notes from Initial Meeting:\n{{ $json.consultationNotes || \"None provided.\" }}\n\nGenerate the complete project plan JSON as instructed in the system prompt. Output ONLY raw JSON, no markdown.",
        "messages": {
          "messageValues": [
            {
              "message": "=You are an expert Agile project manager and Lean Six Sigma Black Belt specializing in legal technology. You receive a project outline and consultation notes, then produce a complete project plan as valid JSON.\n\nOUTPUT ONLY VALID JSON. No markdown fences, no explanation, no extra text \u2014 raw JSON only.\n\nReturn a single JSON object with these top-level keys:\n\n- client: name, company, email, phone, projectTitle, projectType, requestDescription\n- projectVision: problemStatement, businessGoals (array), successMetrics (array), scope (inScope/outOfScope arrays), assumptions (array), constraints (array)\n- leanSixSigma: define (problemStatement, projectCharter, ctq array, sipoc object, voiceOfCustomer array), measure (currentStateDescription, baselineMetrics array), analyze (rootCauses array, fishboneCategories object with 6 keys, prioritizedGaps array), improve (solutions array with solution/impact/effort/description, implementationApproach), control (controlPlan, kpis array, sustainmentActivities array)\n- backlog: array of user stories \u2014 each has id, epic, story, acceptanceCriteria array, priority (Critical/High/Medium/Low), storyPoints, category, sprintTarget\n- roadmap: array of phases \u2014 each has phase, milestone, weeksStart, weeksEnd, deliverables array, colorHex\n- releases: array \u2014 each has version, name, description, sprints array, keyFeatures array, targetWeek\n- sprints: array \u2014 each has number, name, goal, duration, weeksStart, weeksEnd, tasks array (task/type/hours/owner), storyPoints, definitionOfDone array\n- ceremonies: standup (duration/format array/participants array/notes), sprintReview (duration/agenda array/demoChecklist array), retrospective (duration/format/categories with wentWell/needsImprovement/actionItems arrays)\n- budget: development/projectManagement/techSupport/training/travel objects (each with hours, rate, total, description), subtotal, contingency (percentage:10, amount), grandTotal, paymentTerms, notes\n- timeline: totalWeeks, totalSprints, estimatedStartDate, phases array\n- risks: array \u2014 each has id, risk, impact, probability, score, mitigation, owner, status\n\nBe concise. Generate only what is realistic for the actual project scope \u2014 do not pad arrays. 3-5 backlog items, 2-4 sprints, and 3-5 risks is sufficient unless the scope clearly requires more.\n\nBudget calculation rules:\n- Development: $300/hr \u2014 all custom build, automation, integration work\n- Project Management: $150/hr \u2014 typically 15-20% of total project hours\n- Tech Support: $250/hr \u2014 setup, configuration, troubleshooting\n- Training: $500/hr \u2014 includes all course materials\n- Travel: reimbursed at actual cost + $50/hr for travel time. Estimate only if relevant.\n- Contingency: always add 10% of subtotal\n- Generate realistic hour estimates based on the actual scope described\n\nGenerate specific, realistic content based on the actual project outline \u2014 not generic placeholders. Extract client name, company, email, and phone directly from the document text."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.5
    },
    {
      "id": "5956489c-1aa5-450b-a72b-b8993e454b11",
      "name": "Parse Project Plan",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        32
      ],
      "parameters": {
        "jsCode": "var raw = items[0].json.text || items[0].json.output || '';\nraw = raw.trim();\n\n// Strip markdown fences if present\nif (raw.indexOf('```') !== -1) {\n  raw = raw.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n}\n\n// Extract only the JSON object \u2014 everything from first { to last }\nvar start = raw.indexOf('{');\nvar end   = raw.lastIndexOf('}');\nif (start === -1 || end === -1) {\n  throw new Error('No JSON object found in response. Raw start: ' + raw.substring(0, 200));\n}\nraw = raw.substring(start, end + 1);\n\nvar plan;\ntry { plan = JSON.parse(raw); }\ncatch(e) {\n  throw new Error('JSON parse failed: ' + e.message + ' | Raw start: ' + raw.substring(0, 300));\n}\n\nreturn [{ json: plan }];"
      },
      "typeVersion": 2
    },
    {
      "id": "617d2eff-b571-4183-bccd-922f21c285b8",
      "name": "Generate HTML Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        32
      ],
      "parameters": {
        "jsCode": "var p = items[0].json;\nvar c = p.client || {};\nvar v = p.projectVision || {};\nvar lss = p.leanSixSigma || {};\nvar bgt = p.budget || {};\nvar tl  = p.timeline || {};\nvar today = new Date().toISOString().split('T')[0];\nvar safeCompany = (c.company || 'Client').replace(/[^a-zA-Z0-9 ]/g,'').trim().replace(/ +/g,'-');\nvar fn = 'Project-Plan-' + safeCompany + '-' + today + '.pdf';\n\n// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction esc(s){ return (s||'').toString().replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }\nfunction safe(s){ return (s||'').toString(); }\nfunction money(n){ return '$' + (n||0).toLocaleString('en-US'); }\nfunction arr(a){ return Array.isArray(a) ? a : []; }\n\nfunction badge(priority){\n  var colors = {Critical:'#dc2626',High:'#FA4616',Medium:'#f59e0b',Low:'#16a34a'};\n  var col = colors[priority] || '#6b7280';\n  return '<span style=\"background:' + col + ';color:#fff;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:700\">' + esc(priority) + '</span>';\n}\n\nfunction impactBadge(v){\n  var c2 = {High:'#dc2626',Medium:'#f59e0b',Low:'#16a34a'};\n  return '<span style=\"background:' + (c2[v]||'#6b7280') + ';color:#fff;padding:2px 7px;border-radius:8px;font-size:10px;font-weight:700\">' + esc(v) + '</span>';\n}\n\nfunction sectionHead(title, subtitle){\n  return '<div style=\"margin:0 0 18px\"><h2 style=\"color:#002657;font-size:17px;border-bottom:2px solid #FA4616;padding-bottom:6px;margin:0 0 4px\">' + title + '</h2>' +\n    (subtitle ? '<p style=\"color:#6b7280;font-size:12px;margin:0\">' + esc(subtitle) + '</p>' : '') + '</div>';\n}\n\nfunction pb(num){ return '<div style=\"background:#e5e7eb;border-radius:4px;height:6px;margin-top:4px\"><div style=\"background:#FA4616;height:6px;border-radius:4px;width:' + Math.min(100,num) + '%\"></div></div>'; }\n\nfunction listItems(arr2){\n  var out = '<ul style=\"margin:6px 0;padding-left:18px\">';\n  arr(arr2).forEach(function(x){ out += '<li style=\"margin-bottom:3px;font-size:12px\">' + esc(x) + '</li>'; });\n  return out + '</ul>';\n}\n\nfunction twoCol(label, val){\n  return '<tr><td style=\"padding:5px 10px;font-weight:700;color:#002657;border-bottom:1px solid #e5e7eb;font-size:12px;white-space:nowrap\">' + esc(label) + '</td>' +\n    '<td style=\"padding:5px 10px;border-bottom:1px solid #e5e7eb;font-size:12px\">' + esc(val) + '</td></tr>';\n}\n\n// \u2500\u2500 CSS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nvar css = '<style>' +\n'@page{margin:12mm 10mm 14mm}' +\n'*{box-sizing:border-box}' +\n'body{font-family:Arial,Helvetica,sans-serif;margin:0;padding:0;color:#222;font-size:12px;line-height:1.55}' +\n'.hdr{background:#002657;padding:14px 18px;display:flex;align-items:center;justify-content:space-between}' +\n'.hdr img{height:40px}' +\n'.hdr-r{text-align:right;color:#c7c9c8;font-size:11px}' +\n'.hdr-r strong{color:#FA4616;font-size:13px}' +\n'.cover{background:#002657;min-height:260px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:40px 30px;page-break-after:always}' +\n'.cover img{height:60px;margin-bottom:24px}' +\n'.cover h1{color:#fff;font-size:26px;margin:0 0 8px;font-weight:700}' +\n'.cover h2{color:#FA4616;font-size:16px;margin:0 0 16px;font-weight:400}' +\n'.cover .meta{color:#c7c9c8;font-size:12px;line-height:1.8}' +\n'.cover .tag{background:#FA4616;color:#fff;padding:4px 14px;border-radius:12px;font-size:11px;font-weight:700;display:inline-block;margin-top:12px}' +\n'.wrap{padding:18px 0 36px}' +\n'.card{background:#f8f9fb;border:1px solid #e5e7eb;border-radius:6px;padding:14px 16px;margin-bottom:14px}' +\n'.card-blue{background:#eff6ff;border-left:3px solid #0021A5;padding:10px 14px;margin:10px 0;border-radius:0 4px 4px 0}' +\n'.card-orange{background:#fff7f5;border-left:3px solid #FA4616;padding:10px 14px;margin:10px 0;border-radius:0 4px 4px 0}' +\n'.card-green{background:#f0fdf4;border-left:3px solid #16a34a;padding:10px 14px;margin:10px 0;border-radius:0 4px 4px 0}' +\n'table{width:100%;border-collapse:collapse;margin:10px 0;font-size:11.5px}' +\n'th{background:#002657;color:#fff;padding:7px 9px;text-align:left;font-size:11px}' +\n'td{border:1px solid #e5e7eb;padding:6px 9px;vertical-align:top}' +\n'tr:nth-child(even) td{background:#f9fafb}' +\n'h3{color:#0021A5;font-size:13px;margin:14px 0 7px}' +\n'h4{color:#002657;font-size:12px;margin:10px 0 5px}' +\n'.page-break{page-break-before:always}' +\n'.gantt-cell{height:18px;background:#e5e7eb}' +\n'.gantt-fill{height:18px}' +\n'.dmaic-row{display:flex;margin:10px 0}' +\n'.dmaic-phase{flex:1;text-align:center;padding:8px 4px;font-size:10px;font-weight:700;color:#fff}' +\n'.lss-label{font-size:9.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase}' +\n'.budget-total{background:#002657;color:#fff;font-weight:700;font-size:13px}' +\n'.ftr{background:#002657;color:#c7c9c8;text-align:center;padding:8px;font-size:9px;margin:24px -10mm -14mm -10mm}' +\n'</style>';\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// BUILD HTML\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nvar H = '<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Project Plan \u2014 ' + esc(c.company) + '</title>' + css + '</head><body>';\n\n// \u2500\u2500 COVER PAGE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"cover\">';\nH += '<img src=\"https://legalgpts.com/wp-content/uploads/2026/02/lg_whiteorange_clear.png\" alt=\"legalGPTs\">';\nH += '<h1>' + esc(c.projectTitle || 'Project Plan') + '</h1>';\nH += '<h2>' + esc(c.company || '') + '</h2>';\nH += '<div class=\"meta\">';\nH += 'Prepared For: <strong style=\"color:#fff\">' + esc(c.name) + '</strong><br>';\nif(c.email) H += esc(c.email) + '<br>';\nif(c.phone) H += esc(c.phone) + '<br>';\nH += 'Date: ' + today + '<br>';\nH += 'Prepared By: legalGPTs | legalgpts.com';\nH += '</div>';\nH += '<div class=\"tag\">' + esc(c.projectType || 'Development') + ' Engagement</div>';\nH += '</div>';\n\n// \u2500\u2500 HEADER (appears after cover) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"hdr\"><img src=\"https://legalgpts.com/wp-content/uploads/2026/02/lg_orangeblue_clear.png\" alt=\"legalGPTs\">';\nH += '<div class=\"hdr-r\"><strong>Project Plan</strong><br>' + esc(c.company) + ' | ' + today + '</div></div>';\n\n// \u2500\u2500 WRAP START \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"wrap\">';\n\n// \u2500\u2500 1. PROJECT VISION & CHARTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += sectionHead('1. Project Vision & Charter', 'The why behind this engagement');\nH += '<div class=\"card-orange\"><strong>Problem Statement:</strong><br><em>' + esc(v.problemStatement) + '</em></div>';\n\nH += '<div style=\"display:table;width:100%;margin-top:10px\">';\nH += '<div style=\"display:table-cell;width:50%;padding-right:8px;vertical-align:top\">';\nH += '<h4>Business Goals</h4>' + listItems(v.businessGoals);\nH += '<h4>Success Metrics</h4>' + listItems(v.successMetrics);\nH += '</div><div style=\"display:table-cell;width:50%;padding-left:8px;vertical-align:top\">';\nH += '<h4>In Scope</h4>' + listItems(arr(v.scope).length ? v.scope.inScope : []);\nH += '<h4>Out of Scope</h4>' + listItems(arr(v.scope).length ? v.scope.outOfScope : []);\nH += '</div></div>';\n\nif(arr(v.assumptions).length){\n  H += '<h4>Assumptions</h4>' + listItems(v.assumptions);\n}\nif(arr(v.constraints).length){\n  H += '<h4>Constraints</h4>' + listItems(v.constraints);\n}\n\n// \u2500\u2500 2. LEAN SIX SIGMA \u2014 DMAIC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"page-break\">';\nH += sectionHead('2. Lean Six Sigma Framework', 'DMAIC methodology applied to this engagement');\n\n// DMAIC visual bar\nvar dmaicColors = ['#002657','#0021A5','#FA4616','#16a34a','#7c3aed'];\nvar dmaicLabels = ['DEFINE','MEASURE','ANALYZE','IMPROVE','CONTROL'];\nvar dmaicDesc   = ['Problem & Scope','Current State Baseline','Root Cause Analysis','Solution Implementation','Sustain & Monitor'];\nH += '<div class=\"dmaic-row\">';\nfor(var di=0;di<5;di++){\n  H += '<div class=\"dmaic-phase\" style=\"background:' + dmaicColors[di] + ';margin-right:' + (di<4?'2px':'0') + '\">';\n  H += '<div class=\"lss-label\">' + dmaicLabels[di] + '</div>';\n  H += '<div style=\"font-size:9px;margin-top:3px;font-weight:400;color:rgba(255,255,255,0.85)\">' + dmaicDesc[di] + '</div>';\n  H += '</div>';\n}\nH += '</div>';\n\n// DEFINE\nvar def = lss.define || {};\nH += '<h3>Define Phase</h3>';\nH += '<div class=\"card-blue\"><strong>Problem Statement:</strong> ' + esc(def.problemStatement) + '</div>';\nH += '<div class=\"card-blue\"><strong>Project Charter:</strong> ' + esc(def.projectCharter) + '</div>';\n\n// VOC\nif(arr(def.voiceOfCustomer).length){\n  H += '<h4>Voice of Customer (VOC)</h4>' + listItems(def.voiceOfCustomer);\n}\n\n// SIPOC Table\nvar sipoc = def.sipoc || {};\nH += '<h4>SIPOC Diagram</h4>';\nH += '<table><thead><tr>';\n['Suppliers','Inputs','Process','Outputs','Customers'].forEach(function(h){ H += '<th style=\"background:' + ['#002657','#0021A5','#FA4616','#0021A5','#002657'][(['Suppliers','Inputs','Process','Outputs','Customers'].indexOf(h))] + '\">' + h + '</th>'; });\nH += '</tr></thead><tbody>';\nvar sipocMax = Math.max(\n  arr(sipoc.suppliers).length, arr(sipoc.inputs).length,\n  arr(sipoc.process).length, arr(sipoc.outputs).length, arr(sipoc.customers).length\n);\nfor(var si=0;si<sipocMax;si++){\n  H += '<tr>';\n  [sipoc.suppliers, sipoc.inputs, sipoc.process, sipoc.outputs, sipoc.customers].forEach(function(col){\n    H += '<td>' + esc((arr(col)[si])||'') + '</td>';\n  });\n  H += '</tr>';\n}\nH += '</tbody></table>';\n\n// CTQ\nif(arr(def.ctq).length){\n  H += '<h4>Critical-to-Quality (CTQ) Requirements</h4>';\n  H += '<table><thead><tr><th>Requirement</th><th>Specification</th><th>Measurement</th></tr></thead><tbody>';\n  arr(def.ctq).forEach(function(q){\n    H += '<tr><td><strong>' + esc(q.requirement) + '</strong></td><td>' + esc(q.specification) + '</td><td>' + esc(q.measurement) + '</td></tr>';\n  });\n  H += '</tbody></table>';\n}\n\n// MEASURE\nvar meas = lss.measure || {};\nH += '<h3>Measure Phase</h3>';\nH += '<div class=\"card\">' + esc(meas.currentStateDescription) + '</div>';\nif(arr(meas.baselineMetrics).length){\n  H += '<table><thead><tr><th>Metric</th><th>Current State</th><th>Target</th><th>Unit</th></tr></thead><tbody>';\n  arr(meas.baselineMetrics).forEach(function(m){\n    H += '<tr><td>' + esc(m.metric) + '</td><td>' + esc(m.current) + '</td>';\n    H += '<td style=\"color:#16a34a;font-weight:700\">' + esc(m.target) + '</td><td>' + esc(m.unit) + '</td></tr>';\n  });\n  H += '</tbody></table>';\n}\n\n// ANALYZE\nvar anal = lss.analyze || {};\nH += '<h3>Analyze Phase</h3>';\nH += '<h4>Root Causes Identified</h4>' + listItems(anal.rootCauses);\n\n// Fishbone summary table\nvar fb = anal.fishboneCategories || {};\nvar fbCats = ['people','process','technology','data','environment','measurement'];\nH += '<h4>Fishbone (Ishikawa) Cause Categories</h4>';\nH += '<table><thead><tr><th>Category</th><th>Contributing Factors</th></tr></thead><tbody>';\nfbCats.forEach(function(cat){\n  var items2 = arr(fb[cat]);\n  if(items2.length){\n    H += '<tr><td style=\"font-weight:700;color:#002657;text-transform:capitalize\">' + cat + '</td>';\n    H += '<td>' + items2.map(function(x){ return esc(x); }).join(' &bull; ') + '</td></tr>';\n  }\n});\nH += '</tbody></table>';\n\n// IMPROVE\nvar imp = lss.improve || {};\nH += '<h3>Improve Phase</h3>';\nif(arr(imp.solutions).length){\n  H += '<table><thead><tr><th>Solution</th><th>Impact</th><th>Effort</th><th>Description</th></tr></thead><tbody>';\n  arr(imp.solutions).forEach(function(s){\n    H += '<tr><td><strong>' + esc(s.solution) + '</strong></td>';\n    H += '<td>' + impactBadge(s.impact) + '</td>';\n    H += '<td>' + impactBadge(s.effort) + '</td>';\n    H += '<td>' + esc(s.description) + '</td></tr>';\n  });\n  H += '</tbody></table>';\n}\n\n// CONTROL\nvar ctrl = lss.control || {};\nH += '<h3>Control Phase</h3>';\nH += '<div class=\"card-green\"><strong>Control Plan:</strong> ' + esc(ctrl.controlPlan) + '</div>';\nif(arr(ctrl.kpis).length){\n  H += '<h4>KPI Dashboard</h4>';\n  H += '<table><thead><tr><th>KPI</th><th>Target</th><th>Frequency</th><th>Owner</th></tr></thead><tbody>';\n  arr(ctrl.kpis).forEach(function(k){\n    H += '<tr><td><strong>' + esc(k.kpi) + '</strong></td><td style=\"color:#0021A5;font-weight:700\">' + esc(k.target) + '</td>';\n    H += '<td>' + esc(k.frequency) + '</td><td>' + esc(k.owner) + '</td></tr>';\n  });\n  H += '</tbody></table>';\n}\nH += '</div>';\n\n// \u2500\u2500 3. PRODUCT BACKLOG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"page-break\">';\nH += sectionHead('3. Product Backlog', 'Prioritized list of user stories and requirements');\n\nvar backlog = arr(p.backlog);\nvar epics = {};\nbacklog.forEach(function(us){ if(us.epic){ if(!epics[us.epic]) epics[us.epic]=[]; epics[us.epic].push(us); } });\n\nH += '<table><thead><tr><th style=\"width:60px\">ID</th><th>User Story</th><th style=\"width:80px\">Epic</th><th style=\"width:70px\">Priority</th><th style=\"width:40px\">Pts</th><th style=\"width:50px\">Sprint</th></tr></thead><tbody>';\nbacklog.forEach(function(us){\n  H += '<tr><td style=\"font-weight:700;color:#0021A5\">' + esc(us.id) + '</td>';\n  H += '<td><div style=\"font-size:11px\">' + esc(us.story) + '</div>';\n  if(arr(us.acceptanceCriteria).length){\n    H += '<div style=\"font-size:10px;color:#6b7280;margin-top:3px\">AC: ' + arr(us.acceptanceCriteria).map(function(a){return esc(a);}).join('; ') + '</div>';\n  }\n  H += '</td>';\n  H += '<td style=\"font-size:10px\">' + esc(us.epic) + '</td>';\n  H += '<td>' + badge(us.priority) + '</td>';\n  H += '<td style=\"text-align:center;font-weight:700\">' + (us.storyPoints||'') + '</td>';\n  H += '<td style=\"text-align:center\">S' + (us.sprintTarget||'') + '</td></tr>';\n});\nH += '</tbody></table>';\n\n// Story Points Summary\nvar totalPts = backlog.reduce(function(acc,us){ return acc + (us.storyPoints||0); }, 0);\nvar critCount = backlog.filter(function(u){ return u.priority==='Critical'; }).length;\nvar highCount  = backlog.filter(function(u){ return u.priority==='High'; }).length;\nH += '<div class=\"card\" style=\"margin-top:10px\">';\nH += '<div style=\"display:table;width:100%\"><div style=\"display:table-cell;text-align:center\">';\nH += '<strong style=\"font-size:20px;color:#002657\">' + totalPts + '</strong><div style=\"font-size:10px;color:#6b7280\">Total Story Points</div></div>';\nH += '<div style=\"display:table-cell;text-align:center\"><strong style=\"font-size:20px;color:#dc2626\">' + critCount + '</strong><div style=\"font-size:10px;color:#6b7280\">Critical Items</div></div>';\nH += '<div style=\"display:table-cell;text-align:center\"><strong style=\"font-size:20px;color:#FA4616\">' + highCount + '</strong><div style=\"font-size:10px;color:#6b7280\">High Priority</div></div>';\nH += '<div style=\"display:table-cell;text-align:center\"><strong style=\"font-size:20px;color:#0021A5\">' + backlog.length + '</strong><div style=\"font-size:10px;color:#6b7280\">Total User Stories</div></div>';\nH += '</div></div>';\nH += '</div>';\n\n// \u2500\u2500 4. PRODUCT ROADMAP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"page-break\">';\nH += sectionHead('4. Product Roadmap', 'High-level milestones and delivery sequence');\n\nvar roadmap = arr(p.roadmap);\nvar totalWks = tl.totalWeeks || 12;\n\n// Gantt chart \u2014 CSS table\nH += '<table style=\"table-layout:fixed\"><thead><tr>';\nH += '<th style=\"width:28%\">Phase / Milestone</th>';\nfor(var wk=1;wk<=totalWks;wk++){\n  H += '<th style=\"text-align:center;padding:4px 2px;font-size:9px;width:' + (72/totalWks).toFixed(1) + '%\">W' + wk + '</th>';\n}\nH += '</tr></thead><tbody>';\nroadmap.forEach(function(row){\n  H += '<tr>';\n  H += '<td style=\"font-size:11px\"><strong>' + esc(row.phase) + '</strong><br><span style=\"color:#6b7280;font-size:10px\">' + esc(row.milestone) + '</span></td>';\n  for(var wk2=1;wk2<=totalWks;wk2++){\n    var inRange = wk2 >= (row.weeksStart||1) && wk2 <= (row.weeksEnd||1);\n    if(inRange){\n      H += '<td style=\"background:' + (row.colorHex||'#002657') + ';padding:0\"></td>';\n    } else {\n      H += '<td style=\"background:#f3f4f6;padding:0\"></td>';\n    }\n  }\n  H += '</tr>';\n  // Deliverables row\n  if(arr(row.deliverables).length){\n    H += '<tr><td colspan=\"' + (totalWks+1) + '\" style=\"padding:2px 8px 8px 24px;font-size:9.5px;color:#6b7280;background:#fafafa\">';\n    H += '&#x21B3; ' + arr(row.deliverables).map(function(d){ return esc(d); }).join(' &bull; ');\n    H += '</td></tr>';\n  }\n});\nH += '</tbody></table>';\n\n// Legend\nH += '<div style=\"margin-top:8px;display:flex;gap:12px;align-items:center;font-size:10px;color:#6b7280\">';\nroadmap.forEach(function(row){\n  H += '<span><span style=\"display:inline-block;width:12px;height:10px;background:' + (row.colorHex||'#002657') + ';border-radius:2px;margin-right:4px;vertical-align:middle\"></span>' + esc(row.phase) + '</span>';\n});\nH += '</div>';\n\n// \u2500\u2500 5. RELEASE PLANNING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<h2 style=\"color:#002657;font-size:17px;border-bottom:2px solid #FA4616;padding-bottom:6px;margin:20px 0 12px\">5. Release Planning</h2>';\nvar releases = arr(p.releases);\nH += '<table><thead><tr><th style=\"width:60px\">Release</th><th>Name & Description</th><th>Key Features</th><th style=\"width:80px\">Sprints</th><th style=\"width:70px\">Target Week</th></tr></thead><tbody>';\nreleases.forEach(function(r){\n  H += '<tr>';\n  H += '<td style=\"font-weight:700;color:#0021A5;font-size:13px\">' + esc(r.version) + '</td>';\n  H += '<td><strong>' + esc(r.name) + '</strong><br><span style=\"color:#6b7280;font-size:10px\">' + esc(r.description) + '</span></td>';\n  H += '<td><ul style=\"margin:0;padding-left:14px\">' + arr(r.keyFeatures).map(function(f){ return '<li style=\"font-size:10.5px\">' + esc(f) + '</li>'; }).join('') + '</ul></td>';\n  H += '<td style=\"text-align:center\">' + arr(r.sprints).map(function(s){ return 'S'+s; }).join(', ') + '</td>';\n  H += '<td style=\"text-align:center;font-weight:700\">Week ' + (r.targetWeek||'TBD') + '</td>';\n  H += '</tr>';\n});\nH += '</tbody></table>';\nH += '</div>';\n\n// \u2500\u2500 6. SPRINT PLANNING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"page-break\">';\nH += '<div class=\"wrap\">';\nH += sectionHead('6. Sprint Planning & Backlog', 'Iteration plan \u2014 ' + (tl.totalSprints||0) + ' sprints, ' + (tl.totalWeeks||0) + ' weeks total');\n\nvar sprints = arr(p.sprints);\nsprints.forEach(function(sp, si){\n  H += '<div class=\"card\" style=\"margin-bottom:14px;page-break-inside:avoid\">';\n  H += '<div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px\">';\n  H += '<div><strong style=\"color:#002657;font-size:13px\">Sprint ' + sp.number + ': ' + esc(sp.name) + '</strong>';\n  H += '<div style=\"font-size:10px;color:#6b7280\">Weeks ' + sp.weeksStart + '\u2013' + sp.weeksEnd + ' &bull; ' + esc(sp.duration) + '</div></div>';\n  H += '<div style=\"text-align:right\"><strong style=\"font-size:18px;color:#FA4616\">' + (sp.storyPoints||0) + '</strong><div style=\"font-size:9px;color:#6b7280\">story pts</div></div>';\n  H += '</div>';\n  H += '<div class=\"card-blue\" style=\"margin-bottom:8px\"><strong>Sprint Goal:</strong> ' + esc(sp.goal) + '</div>';\n\n  // Tasks table\n  if(arr(sp.tasks).length){\n    H += '<table><thead><tr><th>Task</th><th style=\"width:80px\">Type</th><th style=\"width:50px\">Hours</th><th style=\"width:80px\">Owner</th></tr></thead><tbody>';\n    arr(sp.tasks).forEach(function(t){\n      var typeColors = {Development:'#0021A5',PM:'#002657','Project Management':'#002657',Support:'#7c3aed',Training:'#16a34a'};\n      var tc = typeColors[t.type] || '#6b7280';\n      H += '<tr><td>' + esc(t.task) + '</td>';\n      H += '<td><span style=\"color:' + tc + ';font-weight:700;font-size:10px\">' + esc(t.type) + '</span></td>';\n      H += '<td style=\"text-align:center\">' + (t.hours||'') + '</td>';\n      H += '<td style=\"font-size:10px\">' + esc(t.owner) + '</td></tr>';\n    });\n    H += '</tbody></table>';\n  }\n\n  // Definition of Done\n  if(arr(sp.definitionOfDone).length){\n    H += '<div style=\"margin-top:8px;font-size:10.5px\"><strong style=\"color:#002657\">Definition of Done:</strong> ';\n    H += arr(sp.definitionOfDone).map(function(d){ return '&#x2713; ' + esc(d); }).join(' &bull; ');\n    H += '</div>';\n  }\n  H += '</div>';\n});\n\n// \u2500\u2500 7. AGILE CEREMONIES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += sectionHead('7. Agile Ceremonies', 'Team rituals that drive continuous improvement');\n\nvar cer = p.ceremonies || {};\nH += '<div style=\"display:table;width:100%;border-collapse:separate;border-spacing:8px\">';\n\n// Daily Stand-up\nvar std = cer.standup || {};\nH += '<div style=\"display:table-cell;vertical-align:top;background:#eff6ff;border:1px solid #bfdbfe;border-radius:6px;padding:12px\">';\nH += '<div style=\"background:#0021A5;color:#fff;padding:6px 10px;border-radius:4px;font-size:11px;font-weight:700;margin:-12px -12px 10px\">&#x1F4CB; Daily Stand-up</div>';\nH += '<div style=\"font-size:10px;color:#6b7280;margin-bottom:6px\">' + esc(std.duration||'15 minutes') + '</div>';\nH += '<strong style=\"font-size:11px;color:#002657\">Format:</strong>';\nH += '<ol style=\"margin:6px 0;padding-left:16px\">';\narr(std.format).forEach(function(f){ H += '<li style=\"font-size:11px;margin-bottom:3px\">' + esc(f) + '</li>'; });\nH += '</ol>';\nif(std.notes){ H += '<div style=\"font-size:10px;color:#6b7280;font-style:italic;margin-top:6px\">' + esc(std.notes) + '</div>'; }\nH += '</div>';\n\n// Sprint Review\nvar rev = cer.sprintReview || {};\nH += '<div style=\"display:table-cell;vertical-align:top;background:#fff7f5;border:1px solid #fed7aa;border-radius:6px;padding:12px\">';\nH += '<div style=\"background:#FA4616;color:#fff;padding:6px 10px;border-radius:4px;font-size:11px;font-weight:700;margin:-12px -12px 10px\">&#x1F3AF; Sprint Review & Demo</div>';\nH += '<div style=\"font-size:10px;color:#6b7280;margin-bottom:6px\">' + esc(rev.duration||'1-2 hours') + ' &bull; End of each sprint</div>';\nH += '<strong style=\"font-size:11px;color:#002657\">Agenda:</strong>';\nH += '<ol style=\"margin:6px 0;padding-left:16px\">';\narr(rev.agenda).forEach(function(a){ H += '<li style=\"font-size:11px;margin-bottom:3px\">' + esc(a) + '</li>'; });\nH += '</ol>';\nif(arr(rev.demoChecklist).length){\n  H += '<strong style=\"font-size:11px;color:#002657\">Demo Checklist:</strong>';\n  arr(rev.demoChecklist).forEach(function(d){ H += '<div style=\"font-size:10.5px\">&#x2713; ' + esc(d) + '</div>'; });\n}\nH += '</div>';\n\n// Retrospective\nvar retro = cer.retrospective || {};\nH += '<div style=\"display:table-cell;vertical-align:top;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:12px\">';\nH += '<div style=\"background:#16a34a;color:#fff;padding:6px 10px;border-radius:4px;font-size:11px;font-weight:700;margin:-12px -12px 10px\">&#x1F504; Sprint Retrospective</div>';\nH += '<div style=\"font-size:10px;color:#6b7280;margin-bottom:6px\">' + esc(retro.duration||'1 hour') + ' &bull; ' + esc(retro.format||'Start/Stop/Continue') + '</div>';\nvar rcat = retro.categories || {};\nif(arr(rcat.wentWell).length){\n  H += '<strong style=\"font-size:11px;color:#16a34a\">What Went Well:</strong>';\n  arr(rcat.wentWell).forEach(function(w){ H += '<div style=\"font-size:10.5px\">&#x2713; ' + esc(w) + '</div>'; });\n}\nif(arr(rcat.actionItems).length){\n  H += '<strong style=\"font-size:11px;color:#002657;margin-top:6px;display:block\">Action Items:</strong>';\n  arr(rcat.actionItems).forEach(function(a){ H += '<div style=\"font-size:10.5px\">&#x2192; ' + esc(a) + '</div>'; });\n}\nH += '</div>';\n\nH += '</div>';\nH += '</div>';\n\n// \u2500\u2500 8. BUDGET & PRICING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div class=\"page-break\"><div class=\"wrap\">';\nH += sectionHead('8. Budget & Pricing', 'Detailed cost breakdown based on scope of work');\n\nH += '<table><thead><tr><th>Service Category</th><th>Rate</th><th style=\"width:70px\">Est. Hours</th><th style=\"width:90px\">Total</th><th>Description</th></tr></thead><tbody>';\n\nvar budgetRows = [\n  {key:'development',      label:'Development',        rateStr:'$300/hr'},\n  {key:'projectManagement',label:'Project Management', rateStr:'$150/hr'},\n  {key:'techSupport',      label:'Tech Support',       rateStr:'$250/hr'},\n  {key:'training',         label:'Training',           rateStr:'$500/hr'}\n];\n\nbudgetRows.forEach(function(row){\n  var b = bgt[row.key] || {};\n  if((b.hours||0) > 0 || (b.total||0) > 0){\n    H += '<tr><td><strong>' + row.label + '</strong></td><td style=\"color:#0021A5;font-weight:700\">' + row.rateStr + '</td>';\n    H += '<td style=\"text-align:center\">' + (b.hours||0) + '</td>';\n    H += '<td style=\"text-align:right;font-weight:700\">' + money(b.total) + '</td>';\n    H += '<td style=\"font-size:10.5px\">' + esc(b.description) + '</td></tr>';\n  }\n});\n\nvar tv = bgt.travel || {};\nif((tv.total||0) > 0){\n  H += '<tr><td><strong>Travel</strong></td><td style=\"color:#0021A5;font-weight:700\">Cost + $50/hr</td>';\n  H += '<td style=\"text-align:center\">' + (tv.travelHours||0) + ' travel hrs</td>';\n  H += '<td style=\"text-align:right;font-weight:700\">' + money(tv.total) + '</td>';\n  H += '<td style=\"font-size:10.5px\">' + esc(tv.description) + '</td></tr>';\n}\n\nH += '<tr style=\"background:#f3f4f6\"><td colspan=\"3\" style=\"text-align:right;font-weight:700;color:#002657\">Subtotal</td>';\nH += '<td style=\"text-align:right;font-weight:700\">' + money(bgt.subtotal) + '</td><td></td></tr>';\n\nvar cont = bgt.contingency || {};\nH += '<tr><td colspan=\"3\" style=\"text-align:right;color:#6b7280\">Contingency (' + (cont.percentage||10) + '%)</td>';\nH += '<td style=\"text-align:right\">' + money(cont.amount) + '</td><td style=\"font-size:10px;color:#6b7280\">Risk buffer</td></tr>';\n\nH += '<tr class=\"budget-total\"><td colspan=\"3\" style=\"text-align:right;padding:10px 9px\">PROJECT TOTAL ESTIMATE</td>';\nH += '<td style=\"text-align:right;font-size:16px;color:#FA4616;padding:10px 9px\">' + money(bgt.grandTotal) + '</td><td></td></tr>';\n\nH += '</tbody></table>';\n\nH += '<div style=\"display:table;width:100%;margin-top:12px;gap:10px\">';\nH += '<div style=\"display:table-cell;padding-right:8px\">';\nH += '<div class=\"card-blue\"><strong>Payment Terms:</strong><br>' + esc(bgt.paymentTerms) + '</div>';\nH += '</div><div style=\"display:table-cell;padding-left:8px\">';\nH += '<div class=\"card-orange\"><strong>Rate Schedule:</strong><br><span style=\"font-size:10.5px\">' + esc(bgt.notes) + '</span></div>';\nH += '</div></div>';\n\n// Effort distribution chart (CSS bar chart)\nH += '<h4 style=\"margin-top:14px\">Effort Distribution</h4>';\nvar grandTotal = bgt.grandTotal || 1;\nvar distRows = [\n  {label:'Development',  val:(bgt.development||{}).total||0, color:'#0021A5'},\n  {label:'Project Mgmt', val:(bgt.projectManagement||{}).total||0, color:'#002657'},\n  {label:'Tech Support', val:(bgt.techSupport||{}).total||0, color:'#7c3aed'},\n  {label:'Training',     val:(bgt.training||{}).total||0, color:'#16a34a'},\n  {label:'Travel',       val:(bgt.travel||{}).total||0, color:'#FA4616'}\n];\ndistRows.forEach(function(dr){\n  if(!dr.val) return;\n  var pct = Math.round((dr.val/grandTotal)*100);\n  H += '<div style=\"margin-bottom:8px\">';\n  H += '<div style=\"display:flex;justify-content:space-between;font-size:10.5px;margin-bottom:2px\">';\n  H += '<span>' + dr.label + '</span><span style=\"font-weight:700\">' + money(dr.val) + ' (' + pct + '%)</span></div>';\n  H += '<div style=\"background:#e5e7eb;border-radius:4px;height:10px\">';\n  H += '<div style=\"background:' + dr.color + ';height:10px;border-radius:4px;width:' + pct + '%\"></div></div>';\n  H += '</div>';\n});\n\n// \u2500\u2500 9. RISK REGISTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += sectionHead('9. Risk Register', 'Identified risks and mitigation strategies');\n\nvar risks = arr(p.risks);\nH += '<table><thead><tr><th style=\"width:50px\">ID</th><th>Risk</th><th style=\"width:60px\">Impact</th><th style=\"width:70px\">Probability</th><th style=\"width:60px\">Score</th><th>Mitigation</th><th style=\"width:60px\">Owner</th></tr></thead><tbody>';\nrisks.forEach(function(r){\n  H += '<tr>';\n  H += '<td style=\"font-weight:700;color:#0021A5\">' + esc(r.id) + '</td>';\n  H += '<td>' + esc(r.risk) + '</td>';\n  H += '<td>' + impactBadge(r.impact) + '</td>';\n  H += '<td>' + impactBadge(r.probability) + '</td>';\n  H += '<td>' + impactBadge(r.score) + '</td>';\n  H += '<td style=\"font-size:10.5px\">' + esc(r.mitigation) + '</td>';\n  H += '<td style=\"font-size:10.5px\">' + esc(r.owner) + '</td>';\n  H += '</tr>';\n});\nH += '</tbody></table>';\n\n// \u2500\u2500 SIGNATURE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nH += '<div style=\"margin-top:24px;padding-top:16px;border-top:2px solid #e5e7eb\">';\nH += '<h4>Project Authorization</h4>';\nH += '<div style=\"display:table;width:100%\">';\nH += '<div style=\"display:table-cell;width:50%;padding-right:20px\">';\nH += '<div style=\"border-bottom:1px solid #333;height:36px;margin-bottom:6px\"></div>';\nH += '<div style=\"font-size:11px\"><strong>Client Signature</strong><br>' + esc(c.name) + '<br>' + esc(c.company) + '</div>';\nH += '</div><div style=\"display:table-cell;width:50%;padding-left:20px\">';\nH += '<div style=\"border-bottom:1px solid #333;height:36px;margin-bottom:6px\"></div>';\nH += '<div style=\"font-size:11px\"><strong>legalGPTs Representative</strong><br>user@example.com<br>legalgpts.com</div>';\nH += '</div></div>';\nH += '<div style=\"font-size:10px;color:#6b7280;margin-top:12px;font-style:italic\">This proposal is valid for 30 days from ' + today + '. All estimates are subject to refinement during discovery.</div>';\nH += '</div>';\n\nH += '</div>';\nH += '<div class=\"ftr\">legalGPTs | legalgpts.com | user@example.com | Master AI. Practice Law Smarter</div>';\nH += '</body></html>';\n\nreturn [{json: {htmlContent: H, fileName: fn, clientName: c.name, company: c.company, email: c.contactEmail || c.email || ''}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "4ce9a133-dcd8-4a3e-9d46-b7bbdc9fd5a2",
      "name": "Format HTML for PDF",
      "type": "n8n-nodes-base.code",
      "position": [
        1504,
        32
      ],
      "parameters": {
        "jsCode": "var html = items[0].json.htmlContent;\nvar buf = Buffer.from(html, 'utf8');\nreturn [{\n  json: { fileName: items[0].json.fileName, clientName: items[0].json.clientName, company: items[0].json.company, email: items[0].json.email },\n  binary: { htmlFile: await this.helpers.prepareBinaryData(buf, 'index.html', 'text/html') }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "82be3032-7eb8-44a5-b8c5-d2fd6bea131f",
      "name": "Upload PDF to SharePoint Site",
      "type": "n8n-nodes-base.microsoftSharePoint",
      "position": [
        1904,
        32
      ],
      "parameters": {
        "site": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SHAREPOINT_SITE_ID",
          "cachedResultName": "Admin-Client Intake"
        },
        "folder": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_DRIVE_ITEM_ID",
          "cachedResultName": "Project Proposals"
        },
        "fileName": "={{ $json.fileName }}",
        "operation": "upload",
        "fileContents": "pdfData",
        "requestOptions": {}
      },
      "credentials": {
        "microsoftSharePointOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "48efc38e-b589-4518-94ee-6424c87e0302",
      "name": "Draft Notification Email",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        32
      ],
      "parameters": {
        "jsCode": "var sp = items[0].json;\nvar spUrl = sp.webUrl || '#';\nvar fn = sp.name || 'project-plan.pdf';\nvar plan = $('Parse Project Plan').first().json;\nvar c = plan.client || {};\nvar bgt = plan.budget || {};\nvar today = new Date().toISOString().split('T')[0];\n\nfunction money(n){ return '$' + (n||0).toLocaleString('en-US'); }\n\nvar body = '<div style=\"font-family:Arial,Helvetica,sans-serif;max-width:640px;margin:0 auto;color:#333\">' +\n'<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#002657\"><tr>' +\n'<td style=\"padding:14px 20px\"><img src=\"https://legalgpts.com/wp-content/uploads/2026/02/lg_whiteorange_clear.png\" alt=\"legalGPTs\" style=\"height:38px\"></td>' +\n'<td style=\"padding:14px 20px;text-align:right;color:#c7c9c8;font-size:11px\">Project Plan<br><span style=\"color:#FA4616;font-weight:700\">Master AI. Practice Law Smarter</span></td>' +\n'</tr></table>' +\n'<div style=\"background:#f5f7fa;padding:16px 20px;border-bottom:3px solid #FA4616\">' +\n'<p style=\"margin:0 0 3px;font-size:12px;color:#666\">Generated on ' + today + '</p>' +\n'<h2 style=\"margin:0;color:#002657;font-size:17px\">Project Plan Ready: ' + (c.company||'Client') + '</h2>' +\n'<p style=\"margin:5px 0 0;font-size:12px\">Contact: <strong>' + (c.name||'') + '</strong>' + (c.email ? ' &bull; ' + c.email : '') + '</p>' +\n'</div>' +\n'<div style=\"padding:16px 20px\">' +\n'<table width=\"100%\" cellpadding=\"6\" cellspacing=\"0\" style=\"font-size:12px;border-collapse:collapse;border:1px solid #e0e0e0;margin-bottom:14px\">' +\n'<tr style=\"background:#002657;color:#fff\"><th style=\"padding:7px 10px;text-align:left\">Detail</th><th style=\"padding:7px 10px;text-align:left\">Value</th></tr>' +\n'<tr><td style=\"border-bottom:1px solid #e0e0e0;font-weight:700;color:#002657\">Client</td><td style=\"border-bottom:1px solid #e0e0e0\">' + (c.company||'') + '</td></tr>' +\n'<tr style=\"background:#f9f9f9\"><td style=\"border-bottom:1px solid #e0e0e0;font-weight:700;color:#002657\">Project</td><td style=\"border-bottom:1px solid #e0e0e0\">' + (c.projectTitle||'') + '</td></tr>' +\n'<tr><td style=\"border-bottom:1px solid #e0e0e0;font-weight:700;color:#002657\">Type</td><td style=\"border-bottom:1px solid #e0e0e0\">' + (c.projectType||'') + '</td></tr>' +\n'<tr style=\"background:#f9f9f9\"><td style=\"border-bottom:1px solid #e0e0e0;font-weight:700;color:#002657\">Est. Total</td><td style=\"border-bottom:1px solid #e0e0e0;color:#FA4616;font-weight:700;font-size:14px\">' + money(bgt.grandTotal) + '</td></tr>' +\n'<tr><td style=\"font-weight:700;color:#002657\">Document</td><td><a href=\"' + spUrl + '\" style=\"color:#0021A5\">' + fn + '</a></td></tr>' +\n'</table>' +\n'<table cellpadding=\"0\" cellspacing=\"0\"><tr>' +\n'<td><a href=\"' + spUrl + '\" style=\"background:#FA4616;color:#fff;padding:10px 22px;text-decoration:none;border-radius:4px;font-size:13px;font-weight:700;display:inline-block\">View Project Plan on SharePoint</a></td>' +\n'</tr></table>' +\n'<p style=\"font-size:11px;color:#666;margin-top:14px\">This is a comprehensive project plan generated from the client intake outline. Review and customize before sharing with the client.</p>' +\n'</div>' +\n'<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#002657\"><tr>' +\n'<td style=\"padding:10px 20px;font-size:10px;color:#c7c9c8;text-align:center\">legalGPTs | legalgpts.com | Master AI. Practice Law Smarter</td>' +\n'</tr></table></div>';\n\nreturn [{json:{\n  emailSubject: 'Project Plan Ready: ' + (c.company||'Client') + ' \u2014 ' + (c.projectTitle||''),\n  emailBody: body\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "adf4b2bb-c69a-49a7-a6e8-3a9657950ce7",
      "name": "Extract Insights with Claude Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        672,
        176
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-opus-4-6",
          "cachedResultName": "Claude Opus 4.6"
        },
        "options": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "a385fd79-4f50-400d-8d35-ccb603ac1575",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        176,
        32
      ],
      "parameters": {
        "options": {},
        "operation": "pdf",
        "binaryPropertyName": "outline"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d65f96bf-f548-4988-9e21-469d8ea3485a",
      "name": "Merge PDF and AI Data",
      "type": "n8n-nodes-base.code",
      "position": [
        400,
        32
      ],
      "parameters": {
        "jsCode": "var pdfText = items[0].json.text || '';\nvar notes   = items[0].json.notes || $input.first().json.body?.notes || '';\n\nreturn [{\n  json: {\n    pdfText:           pdfText.trim(),\n    consultationNotes: notes.toString().trim()\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8c632820-309a-436b-b926-5e2f6b860e96",
      "name": "Convert HTML to PDF",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1664,
        32
      ],
      "parameters": {
        "url": "https://YOUR_SERVICE_URL",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "pdfData"
            }
          }
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "files",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "htmlFile"
            },
            {
              "name": "preferCssPageSize",
              "value": "true"
            }
          ]
        },
        "genericAuthType": "httpBasicAuth"
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "bf90c32e-e8da-41c6-8ca7-dd8f25817462",
      "name": "Send Email Notification via Outlook",
      "type": "n8n-nodes-base.microsoftOutlook",
      "position": [
        2336,
        32
      ],
      "parameters": {
        "subject": "={{ $json.emailSubject }}",
        "bodyContent": "={{ $json.emailBody }}",
        "toRecipients": "user@example.com",
        "additionalFields": {
          "bodyContentType": "html"
        }
      },
      "credentials": {
        "microsoftOutlookOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "5d27b93c-7f99-4d36-a13d-cd1eeadc2644",
  "nodeGroups": [],
  "connections": {
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "Merge PDF and AI Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Project Plan": {
      "main": [
        [
          {
            "node": "Generate HTML Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert HTML to PDF": {
      "main": [
        [
          {
            "node": "Upload PDF to SharePoint Site",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format HTML for PDF": {
      "main": [
        [
          {
            "node": "Convert HTML to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate HTML Report": {
      "main": [
        [
          {
            "node": "Format HTML for PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge PDF and AI Data": {
      "main": [
        [
          {
            "node": "Project Plan Generator Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Draft Notification Email": {
      "main": [
        [
          {
            "node": "Send Email Notification via Outlook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Project Plan Uploaded": {
      "main": [
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Project Plan Generator Agent": {
      "main": [
        [
          {
            "node": "Parse Project Plan",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload PDF to SharePoint Site": {
      "main": [
        [
          {
            "node": "Draft Notification Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Insights with Claude Model": {
      "ai_languageModel": [
        [
          {
            "node": "Project Plan Generator Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow receives a project outline PDF via webhook, extracts its text, uses Anthropic Claude to generate a structured project plan, renders it into an HTML report, converts it to PDF via an HTTP service, uploads it to Microsoft SharePoint, and sends an Outlook email…

Source: https://n8n.io/workflows/18075/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow receives GDPR data subject access requests via a webhook, searches Microsoft 365 for related emails and documents using Microsoft Graph eDiscovery and SharePoint search, drafts a DPO cov

HTTP Request, Microsoft SharePoint, Chain Llm +3
AI & RAG

Tired of grinding out YouTube content? This n8n workflow turns AI into your personal video factory—creating engaging, faceless shorts on autopilot. Perfect for creators, marketers, or side-hustlers lo

HTTP Request, Google Drive, Google Sheets +6
AI & RAG

Faceless YouTube Generator. Uses httpRequest, limit, googleDrive, googleSheets. Webhook trigger; 49 nodes.

HTTP Request, Google Drive, Google Sheets +7
AI & RAG

Marketing, growth, and automation teams that need to ship polished bilingual newsletters—complete with images, optional video, and multi-channel distribution—without writing a line of code. A Webhook

Agent, Microsoft SharePoint, HTTP Request +4
AI & RAG

This workflow receives an intake-form webhook, validates and enriches firm details, uses OpenAI and SerpAPI to research AI governance obligations, generates four branded policy documents, converts the

Agent, OpenAI Chat, Tool Serp Api +3