{
  "name": "WebCentral Revisions Intelligence Pipeline",
  "nodes": [
    {
      "parameters": {},
      "id": "9b09fa66-3c0f-4566-b933-6edf0f99abb5",
      "name": "PADS Form Submission",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        96
      ]
    },
    {
      "parameters": {
        "jsCode": "return [\n  {\n    json: {\n      poc_email: \"jsmith@westminsterco.gov\",\n      poc_signature: \"Janet Smith, Communications Director, City of Westminster CO\",\n      municipality_name_raw: \"Westminster CO\",\n      state: \"Colorado\",\n      pm_selected: \"Sarah Chen\",\n      approval_status: \"No, I'd like to see a few changes first\",\n      raw_revisions: \"1. Change the header background to dark navy and make the logo bigger, also the font in the nav looks too small. 2. The about us page needs a new photo and the staff bios should be moved below the mission statement. 3. Add a search bar to the homepage. We also want to add a live chat widget and integrate our Facebook feed.\",\n      submission_timestamp: \"2024-06-10T14:32:00Z\"\n    }\n  }\n];"
      },
      "id": "842d9eb1-3bde-4442-88cc-242ac82c0331",
      "name": "Raw PADS Form Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        96
      ],
      "notesInFlow": true,
      "notes": "Raw input from PADS Form."
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nreturn [\n  {\n    json: {\n      ...input,\n      project_id: \"WC-2024-0847\",\n      project_tier: \"standard\",\n      pm_name: \"Sarah Chen\",\n      art_director: \"Marcus Webb\",\n      sharepoint_url: \"https://civicplus.sharepoint.com/sites/webcentral/westminster-co\",\n      pads_selections: {\n        template: \"Modern Municipal\",\n        primary_color: \"#1B3A6B\",\n        accent_color: \"#C8A951\",\n        logo_provided: true\n      },\n      match_method: \"email_exact\",\n      match_confidence: 1.0\n    }\n  }\n];"
      },
      "id": "0e6e41ec-b487-4a9f-9086-675639a86db5",
      "name": "Cloud Coach API (Stubbed)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        96
      ],
      "notesInFlow": true,
      "notes": "CloudCoach Project Data Pull"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// In production:\n// 1. Use sharepoint_url from Cloud Coach to call Graph API\n// 2. GET /sites/{site-id}/onenote/notebooks to find the notebook in the project root\n// 3. GET /notebooks/{notebook-id}/sections to find or create the Revisions section\n// 4. Return notebook_id and section_id for the write step\n\nreturn [\n  {\n    json: {\n      ...input,\n      onenote_notebook_id: \"1-abc123def456\",\n      onenote_section_id: \"1-section789xyz\",\n      onenote_section_name: \"Revisions\",\n      onenote_base_url: \"https://civicplus.sharepoint.com/sites/webcentral/westminster-co/_onenote\"\n    }\n  }\n];"
      },
      "id": "4fe76807-c880-43b2-a8c6-79c53b84e2fa",
      "name": "Locate OneNote Notebook (Stubbed)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        672,
        96
      ],
      "notesInFlow": true,
      "notes": "In production: Graph API call to find OneNote notebook in SharePoint project root folder"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nconst scopeRuleset = `SCOPE RULESET - WebCentral Design Revision Review\n\nSTRUCTURAL CHANGES (always route to PM review):\n- Any change to the approved wireframe layout\n- Requesting new sections not present in the approved design\n- Changes to footer structure or layout\n- Moving major elements significantly on the page\n- Changes to page count or sitemap structure\n\nDESIGN CHANGES (in scope, route to art director):\n- Color adjustments within the approved palette\n- Font sizing or spacing tweaks within approved selections\n- Logo swap if customer provides new file\n- Minor alignment or spacing corrections\n- Photo swaps within existing layout\n\nBUTTON AND COMPONENT CHANGES:\n- 1 to 2 individual button or component style adjustments: in scope\n- Request for completely new button style or full component set: out of scope, flag for PM review\n\nALWAYS FLAG FOR PM REVIEW:\n- Any request that would require changes to the approved wireframe\n- Full replacement of approved component selections\n- Anything that reads as a new design direction rather than a refinement`;\n\nconst projectRecord = `PROJECT RECORD - ${input.municipality_name_raw}\nProject ID: ${input.project_id}\nTier: ${input.project_tier}\nPM: ${input.pm_name}\nArt Director: ${input.art_director}\nTemplate: ${input.pads_selections.template}\nPrimary Color: ${input.pads_selections.primary_color}\nAccent Color: ${input.pads_selections.accent_color}\nLogo Provided: ${input.pads_selections.logo_provided}`;\n\nconst userPrompt = \"SCOPE RULESET:\\n\" + scopeRuleset + \"\\n\\nPROJECT RECORD:\\n\" + projectRecord + \"\\n\\nRAW REVISION SUBMISSION:\\n\" + input.raw_revisions + \"\\n\\nParse the raw revision submission into individual items. For each item classify it as design, dev, or ambiguous. Check against the scope ruleset and project record. Return ONLY valid JSON, no markdown, no explanation: {\\\"items\\\":[{\\\"id\\\":1,\\\"text\\\":\\\"cleaned revision text\\\",\\\"type\\\":\\\"design\\\",\\\"in_scope\\\":true,\\\"scope_flag\\\":null,\\\"confidence\\\":0.95}],\\\"batch_confidence\\\":0.95,\\\"scope_flags_present\\\":false,\\\"draft_pm_email\\\":null}\";\n\nreturn [\n  {\n    json: {\n      ...input,\n      request_body: {\n        model: \"anthropic/claude-haiku-4-5\",\n        max_tokens: 2000,\n        messages: [\n          {\n            role: \"system\",\n            content: \"You are a revision intake assistant. You always respond with valid JSON only, no markdown, no explanation.\"\n          },\n          {\n            role: \"user\",\n            content: userPrompt\n          }\n        ]\n      }\n    }\n  }\n];"
      },
      "id": "5a7d67b3-8542-4294-b53e-169d95dd4a0b",
      "name": "Assemble RAG Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        96
      ],
      "notesInFlow": true,
      "notes": "Rag context"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://openrouter.ai/api/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openRouterApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.request_body }}",
        "options": {}
      },
      "id": "0ea9fec7-8c66-4a92-b989-16061dbf67c9",
      "name": "LLM Classification (OpenRouter)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1120,
        96
      ],
      "credentials": {
        "openRouterApi": {
          "id": "1IYXdEEeOeLut63I",
          "name": "OpenRouter account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// Pull upstream fields directly from Cloud Coach node to avoid drop-off through HTTP Request\nconst upstream = $('Cloud Coach API (Stubbed)').first().json;\n\n// Extract and clean LLM response\nlet rawContent = input.choices[0].message.content;\nrawContent = rawContent.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n\nlet parsed;\ntry {\n  parsed = JSON.parse(rawContent);\n} catch(e) {\n  throw new Error(\"Failed to parse LLM response as JSON: \" + rawContent);\n}\n\nconst needsPMReview = parsed.batch_confidence < 0.75 || parsed.scope_flags_present === true;\n\nreturn [\n  {\n    json: {\n      customer_name: upstream.municipality_name_raw,\n      municipality_name_raw: upstream.municipality_name_raw,\n      submission_timestamp: upstream.submission_timestamp,\n      project_id: upstream.project_id,\n      project_tier: upstream.project_tier,\n      pm_name: upstream.pm_name,\n      art_director: upstream.art_director,\n      sharepoint_url: upstream.sharepoint_url,\n      match_confidence: upstream.match_confidence,\n      onenote_notebook_id: $('Locate OneNote Notebook (Stubbed)').first().json.onenote_notebook_id,\n      onenote_section_id: $('Locate OneNote Notebook (Stubbed)').first().json.onenote_section_id,\n      onenote_base_url: $('Locate OneNote Notebook (Stubbed)').first().json.onenote_base_url,\n      items: parsed.items,\n      batch_confidence: parsed.batch_confidence,\n      scope_flags_present: parsed.scope_flags_present,\n      draft_pm_email: parsed.draft_pm_email,\n      needs_pm_review: needsPMReview,\n      routing_reason: needsPMReview\n        ? `Routed to PM: confidence ${parsed.batch_confidence}, scope flags ${parsed.scope_flags_present}`\n        : `Auto-routed to AD: confidence ${parsed.batch_confidence}, no scope flags`\n    }\n  }\n];"
      },
      "id": "6c555ca6-c691-48c5-ae6c-be94759dff90",
      "name": "Parse LLM Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1344,
        96
      ]
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nconst customerName = input.customer_name || input.municipality_name_raw || \"Unknown Customer\";\nconst submissionDate = new Date(input.submission_timestamp);\nconst submissionDateStr = submissionDate.toLocaleDateString('en-US', {\n  year: 'numeric', month: 'long', day: 'numeric'\n});\n\nlet dueDateStr = \"TBD - Pending PM Scope Review\";\nif (!input.scope_flags_present) {\n  const hasDesignItems = input.items.some(i => i.type === 'design' || i.type === 'ambiguous');\n  const hasDevItems = input.items.some(i => i.type === 'dev');\n  const businessDaysToAdd = (hasDesignItems && hasDevItems) ? 10 : 5;\n\n  let daysAdded = 0;\n  let dueDate = new Date(submissionDate);\n  while (daysAdded < businessDaysToAdd) {\n    dueDate.setDate(dueDate.getDate() + 1);\n    const day = dueDate.getDay();\n    if (day !== 0 && day !== 6) daysAdded++;\n  }\n  dueDateStr = dueDate.toLocaleDateString('en-US', {\n    year: 'numeric', month: 'long', day: 'numeric'\n  });\n}\n\nconst infoTable = [\n  { field: \"Date Submitted\", value: submissionDateStr },\n  { field: \"Cloud Coach Link\", value: `https://civicplus.cloudcoach.com/projects/${input.project_id}` },\n  { field: \"SharePoint Folder\", value: input.sharepoint_url },\n  { field: \"Expected Due Date\", value: dueDateStr }\n];\n\nconst revisionTable = input.items.map(item => ({\n  category: item.type.charAt(0).toUpperCase() + item.type.slice(1),\n  request: item.text,\n  file_attached: \"No\",\n  status: \"Pending\",\n  responsibility: item.type === 'dev' ? 'Developer' : item.type === 'design' ? 'Designer' : 'TBD',\n  notes: item.scope_flag ? `SCOPE FLAG: ${item.scope_flag}` : \"\"\n}));\n\nconst pageTitle = `DRAFT - Revisions - ${customerName} - ${submissionDateStr}`;\n\n// In production: POST to Microsoft Graph API\n// POST https://graph.microsoft.com/v1.0/sites/{site-id}/onenote/sections/{section-id}/pages\nconst oneNotePayload = {\n  title: pageTitle,\n  notebook_id: input.onenote_notebook_id,\n  section_id: input.onenote_section_id,\n  info_table: infoTable,\n  revision_table: revisionTable,\n  draft: true,\n  needs_pm_review: input.needs_pm_review\n};\n\nreturn [\n  {\n    json: {\n      ...input,\n      customer_name: customerName,\n      due_date: dueDateStr,\n      onenote_page_title: pageTitle,\n      onenote_payload: oneNotePayload,\n      onenote_written: true\n    }\n  }\n];"
      },
      "id": "f0b94826-bbd3-4b33-8c1e-5cf6bc8aaeac",
      "name": "Write OneNote Page (Stubbed)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        96
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.needs_pm_review }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "false"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Auto-Route to Art Director"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 3
                },
                "conditions": [
                  {
                    "leftValue": "={{ $json.needs_pm_review }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "true"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "PM Review Required"
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "2567ab7e-be7d-4a50-a513-4244413e0615",
      "name": "Route by Confidence",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1792,
        96
      ]
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// In production: Cloud Coach API call to create and assign ticket to art director\nconst ticket = {\n  assigned_to: input.art_director,\n  project_id: input.project_id,\n  customer_name: input.customer_name,\n  ticket_type: \"Design Revision\",\n  onenote_page: input.onenote_page_title,\n  onenote_url: input.onenote_base_url,\n  items: input.items.map(i => ({\n    text: i.text,\n    type: i.type,\n    confidence: i.confidence\n  })),\n  status: \"Open\",\n  created_at: new Date().toISOString()\n};\n\nreturn [\n  {\n    json: {\n      ...input,\n      ticket_created: true,\n      ticket_payload: ticket,\n      status: \"Auto-routed to Art Director\"\n    }\n  }\n];"
      },
      "id": "bd644071-cda1-43da-824b-9c9ad464d07b",
      "name": "Create Ticket in CC for Art Director",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// In production: Graph API call to send Teams or Outlook notification to PM\nconst notification = {\n  to: input.pm_name,\n  subject: `Revisions Submitted to AD - ${input.customer_name}`,\n  body: `Revisions for ${input.customer_name} have been classified and assigned to ${input.art_director}.\\n\\nRouting: ${input.routing_reason}\\n\\nDue Date: ${input.due_date}\\n\\nOneNote Page: ${input.onenote_page_title}\\n${input.onenote_base_url}\\n\\nCloud Coach: https://civicplus.cloudcoach.com/projects/${input.project_id}`,\n  notification_type: \"AD_ASSIGNMENT\"\n};\n\nreturn [\n  {\n    json: {\n      ...input,\n      pm_notified: true,\n      pm_notification_payload: notification,\n      status: \"Complete - AD assigned, PM notified\"\n    }\n  }\n];"
      },
      "id": "687cb8f3-5091-4857-ade1-51306c3e977e",
      "name": "Notify PM of AD Assignment (Stubbed)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2240,
        0
      ],
      "notesInFlow": true,
      "notes": "In production: Microsoft Graph API Teams/Outlook notification to PM"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// In production: Graph API call to send Teams or Outlook notification to PM\nconst flaggedItems = input.items.filter(i => !i.in_scope).map(i => `- ${i.text} (${i.scope_flag})`).join('\\n');\n\nconst notification = {\n  to: input.pm_name,\n  subject: `Revision Review Required - ${input.customer_name}`,\n  body: `A revision batch for ${input.customer_name} requires your review before it can be assigned.\\n\\nRouting Reason: ${input.routing_reason}\\n\\nScope Flags Present: ${input.scope_flags_present}\\n\\nFlagged Items:\\n${flaggedItems}\\n\\nDraft PM Email to Customer:\\n${input.draft_pm_email}\\n\\nOneNote Draft Page: ${input.onenote_page_title}\\n${input.onenote_base_url}\\n\\nCloud Coach: https://civicplus.cloudcoach.com/projects/${input.project_id}`,\n  notification_type: \"PM_REVIEW_REQUIRED\"\n};\n\nreturn [\n  {\n    json: {\n      ...input,\n      notification_sent: true,\n      notification_payload: notification,\n      status: \"Routed to PM for review\"\n    }\n  }\n];"
      },
      "id": "0570a8f8-530d-4468-ac66-ebc5b259dced",
      "name": "PM Review Notification (Stubbed)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        192
      ],
      "notesInFlow": true,
      "notes": "In production: Microsoft Graph API Teams/Outlook notification to PM"
    }
  ],
  "pinData": {},
  "connections": {
    "PADS Form Submission": {
      "main": [
        [
          {
            "node": "Raw PADS Form Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Raw PADS Form Payload": {
      "main": [
        [
          {
            "node": "Cloud Coach API (Stubbed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cloud Coach API (Stubbed)": {
      "main": [
        [
          {
            "node": "Locate OneNote Notebook (Stubbed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Locate OneNote Notebook (Stubbed)": {
      "main": [
        [
          {
            "node": "Assemble RAG Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble RAG Context": {
      "main": [
        [
          {
            "node": "LLM Classification (OpenRouter)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM Classification (OpenRouter)": {
      "main": [
        [
          {
            "node": "Parse LLM Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse LLM Response": {
      "main": [
        [
          {
            "node": "Write OneNote Page (Stubbed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write OneNote Page (Stubbed)": {
      "main": [
        [
          {
            "node": "Route by Confidence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Confidence": {
      "main": [
        [
          {
            "node": "Create Ticket in CC for Art Director",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "PM Review Notification (Stubbed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Ticket in CC for Art Director": {
      "main": [
        [
          {
            "node": "Notify PM of AD Assignment (Stubbed)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": true
  },
  "versionId": "9460d7be-3f14-45b3-bac3-e526847b0736",
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp",
    "instanceId": "60d7d54f88afae1d9ed88fa087b49df3c49eb359a2f99d76a0129255b907ba2b"
  },
  "nodeGroups": [],
  "id": "tKhhXvXlUq27di4m",
  "tags": []
}