{
  "id": "J7pc3fcPtm5rmjqn",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Emergency Equipment Rental Sourcing and Approval Workflow",
  "tags": [],
  "nodes": [
    {
      "id": "c37ab483-c33a-434d-9eaf-7d7df44c11b1",
      "name": "\ud83d\udccb Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        -304
      ],
      "parameters": {
        "width": 664,
        "height": 560,
        "content": "## \ud83c\udfd7\ufe0f Equipment Breakdown \u2014 Automated Rental Sourcing\n\n### How it works\nWhen a site foreman reports a machine breakdown via Telegram (or a web form), this workflow springs into action. It parses the request, fires rental quote emails to five vendors simultaneously, waits for responses, then feeds all quotes into an AI model that compares them and produces a ranked recommendation. The project manager receives a single approval email with a comparison table and one-click Approve / Reject buttons. On approval, the winning vendor gets a booking confirmation and the foreman gets a Telegram update.\n\n### Setup steps\n1. **Telegram Bot** \u2014 connect your bot credentials under *Telegram: Breakdown Report* and update the webhook.\n2. **Gmail OAuth2** \u2014 link your Google account to all three Gmail nodes (Send Vendor Emails, Email PM, Confirm Booking).\n3. **Azure OpenAI** \u2014 add your Azure endpoint and API key to the *Azure OpenAI Chat Model* credential.\n4. **Vendor list** \u2014 open *Split Into Vendor Items* and replace the placeholder vendor names and emails with your actual suppliers.\n5. **PM email** \u2014 in *Email PM: Comparison + Approval* and *Email: Rejection Notice*, replace the recipient address with your project manager's email.\n6. **Approval webhook URL** \u2014 in *Parse AI Analysis*, replace `YOUR-N8N-INSTANCE` with your live n8n domain.\n7. Do a test run using the Telegram command: `/breakdown Excavator | Site A | 3`"
      },
      "typeVersion": 1
    },
    {
      "id": "f0be476d-ae48-4c10-98a4-3a895b77b8ed",
      "name": "Section: Intake",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        240
      ],
      "parameters": {
        "color": 7,
        "width": 536,
        "height": 580,
        "content": "## \ud83d\udce1 Intake & Normalisation\nAccepts breakdown reports from two entry points \u2014 a Telegram bot command or a web form \u2014 and normalises both into a single consistent data shape. Generates a unique Breakdown ID and timestamps the report before anything else runs."
      },
      "typeVersion": 1
    },
    {
      "id": "7325b770-0142-41a4-8e30-18781ef26e60",
      "name": "Section: Vendor Outreach",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 920,
        "height": 596,
        "content": "## \ud83d\udce7 Vendor Outreach\nSplits the request into one item per vendor and sends each a formatted HTML quote-request email via Gmail. The workflow then pauses and waits for replies before proceeding \u2014 giving vendors a defined window to respond."
      },
      "typeVersion": 1
    },
    {
      "id": "5d011454-df46-474c-b331-5631a0724e7d",
      "name": "Section: AI Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 816,
        "height": 728,
        "content": "## \ud83e\udd16 AI Quote Analysis\nCollects all vendor responses, filters out unavailable suppliers, then passes the shortlist to an Azure OpenAI model. The AI returns a structured JSON payload containing an HTML comparison table, a recommended vendor with rationale, risk flags, and an executive summary \u2014 ready to drop straight into the PM's approval email."
      },
      "typeVersion": 1
    },
    {
      "id": "e097eda8-4b74-4324-a36f-8a28f6c70f89",
      "name": "Section: Approval & Booking",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2912,
        96
      ],
      "parameters": {
        "color": 7,
        "width": 864,
        "height": 852,
        "content": "## \u2705 PM Approval & Booking\nEmails the project manager a full vendor comparison with one-click Approve / Reject links. On approval, a booking confirmation goes to the winning vendor and the foreman receives a Telegram notification. On rejection, a manual-review alert is sent instead."
      },
      "typeVersion": 1
    },
    {
      "id": "eb315088-3457-4011-ba04-4dc9344fcc7b",
      "name": "Credentials & Security",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3520,
        1024
      ],
      "parameters": {
        "color": 3,
        "width": 280,
        "height": 256,
        "content": "## \ud83d\udd10 Credentials & Security\nUse OAuth2 for Gmail and a scoped Azure OpenAI API key. Store all secrets in n8n credentials \u2014 never hardcode them in code nodes. Replace all placeholder emails and vendor addresses before publishing or sharing this template."
      },
      "typeVersion": 1
    },
    {
      "id": "8cccc723-7c50-4503-9a86-d730fe1865a7",
      "name": "Telegram: Breakdown Report",
      "type": "n8n-nodes-base.telegramTrigger",
      "notes": "Listens for /breakdown command from foreman. Message format: /breakdown [machine_type] | [site_name] | [duration_days]",
      "position": [
        624,
        496
      ],
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "026cd13b-4989-413a-8805-22e10e81c05c",
      "name": "Normalise Intake Data",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        496
      ],
      "parameters": {
        "jsCode": "// Normalise data from either Telegram or Form trigger\nconst item = $input.first();\nconst data = item.json;\n\nlet machineType, siteName, durationDays, urgency, foremanName, foremanChatId;\n\n// Detect source: Telegram has 'message' key, Form has 'formTitle'\nif (data.message) {\n  // Parse Telegram message: /breakdown [machine_type] | [site_name] | [duration_days]\n  const text = data.message.text || '';\n  const parts = text.replace('/breakdown', '').trim().split('|').map(s => s.trim());\n  machineType = parts[0] || 'Unknown Machine';\n  siteName = parts[1] || 'Unknown Site';\n  durationDays = parseInt(parts[2]) || 1;\n  urgency = 'Critical - Needed Today';\n  foremanName = data.message.from?.first_name + ' ' + (data.message.from?.last_name || '');\n  foremanChatId = String(data.message.chat?.id || '');\n} else {\n  // Form submission\n  machineType = data['Machine Type'] || 'Unknown Machine';\n  siteName = data['Site Name'] || 'Unknown Site';\n  durationDays = parseInt(data['Duration Needed (Days)']) || 1;\n  urgency = data['Urgency'] || 'Standard - Needed Within 48 Hours';\n  foremanName = data['Foreman Name'] || 'Foreman';\n  foremanChatId = data['Foreman Telegram Chat ID'] || '';\n}\n\nconst breakdownId = 'BRK-' + Date.now();\nconst reportedAt = new Date().toISOString();\n\nreturn [{\n  json: {\n    breakdownId,\n    machineType,\n    siteName,\n    durationDays,\n    urgency,\n    foremanName,\n    foremanChatId,\n    reportedAt,\n    vendorQuotes: [],\n    source: data.message ? 'telegram' : 'form'\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a633e8cc-9b1c-4150-bbdb-51e7420ff6c4",
      "name": "Split Into Vendor Items",
      "type": "n8n-nodes-base.code",
      "position": [
        1120,
        496
      ],
      "parameters": {
        "jsCode": "// Build individual vendor email payloads\n// Replace these with your actual vendor emails and names\nconst vendors = [\n  { name: 'Alpha Heavy Equipment Rentals', email: 'rentals@example-alpha.com' },\n  { name: 'Beta Crane Hire Co.', email: 'hire@example-beta.com' },\n  { name: 'Gamma Machinery Rentals', email: 'info@example-gamma.com' },\n  { name: 'Delta Equipment Solutions', email: 'quotes@example-delta.com' },\n  { name: 'Sigma Plant Hire', email: 'plant@example-sigma.com' }\n];\n\nconst intake = $input.first().json;\n\nreturn vendors.map(vendor => ({\n  json: {\n    ...intake,\n    vendor\n  }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "421d273f-e009-469a-a676-727b915585a4",
      "name": "Send Vendor Quote Request Emails",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1344,
        496
      ],
      "parameters": {
        "sendTo": "=vendor@example.com",
        "message": "=<html>\n<body style=\"font-family: Arial, sans-serif; color: #333;\">\n  <h2 style=\"color: #c0392b;\">\u26a0\ufe0f Urgent Equipment Rental Request</h2>\n  <p>Dear {{ $json.vendor.name }} Team,</p>\n  <p>We have an emergency equipment breakdown and require an immediate rental replacement. Please reply to this email with your availability and pricing as soon as possible.</p>\n  \n  <table style=\"border-collapse: collapse; width: 100%; margin: 20px 0;\">\n    <tr style=\"background: #f2f2f2;\"><th style=\"padding: 10px; border: 1px solid #ddd; text-align: left;\">Field</th><th style=\"padding: 10px; border: 1px solid #ddd; text-align: left;\">Details</th></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Reference ID</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $json.breakdownId }}</td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Machine Required</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $json.machineType }}</td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Site Location</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $json.siteName }}</td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Duration Needed</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $json.durationDays }} day(s)</td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Urgency</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong style=\"color: #c0392b;\">{{ $json.urgency }}</strong></td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Reported At</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $json.reportedAt }}</td></tr>\n  </table>\n  \n  <p><strong>Please reply with:</strong></p>\n  <ol>\n    <li>Availability (Yes / No / Partial)</li>\n    <li>Daily rental rate (inc. delivery)</li>\n    <li>Earliest delivery time to site</li>\n    <li>Any additional conditions</li>\n  </ol>\n  \n  <p style=\"color: #888; font-size: 12px;\">This is an automated request. Please reply directly to this email. Reference ID: {{ $json.breakdownId }}</p>\n</body>\n</html>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\ud83d\udea8 Urgent Rental Request \u2013 {{ $json.machineType }} | {{ $json.siteName }} | Ref: {{ $json.breakdownId }}",
        "operation": "sendAndWait"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "0b1eebb1-ab4f-4c97-ab1d-f29ac4ef508c",
      "name": "Aggregate Vendor Quotes",
      "type": "n8n-nodes-base.code",
      "position": [
        2096,
        480
      ],
      "parameters": {
        "jsCode": "// Aggregate all vendor quotes collected during the wait period\n// In production, quotes arrive via the webhook and are stored in a Google Sheet or memory\n// This node reads them and compiles them for GPT-4o\n\nconst intake = $input.first().json;\n\n// Sample structure - in production these come from webhook POSTs\n// Replace with actual $json.body data from webhook\nconst collectedQuotes = intake.vendorQuotes && intake.vendorQuotes.length > 0\n  ? intake.vendorQuotes\n  : [\n      // Fallback demo data structure if no real quotes yet\n      { vendor: 'Alpha Heavy Equipment Rentals', available: true, dailyRate: 4500, deliveryHours: 4, notes: 'Includes operator, fuel extra' },\n      { vendor: 'Beta Crane Hire Co.', available: true, dailyRate: 3900, deliveryHours: 6, notes: 'No operator, standard insurance' },\n      { vendor: 'Gamma Machinery Rentals', available: false, dailyRate: null, deliveryHours: null, notes: 'Not available this week' }\n    ];\n\nconst availableQuotes = collectedQuotes.filter(q => q.available !== false);\n\nreturn [{\n  json: {\n    ...intake,\n    collectedQuotes,\n    availableQuotes,\n    quotesCollectedAt: new Date().toISOString()\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1c6998d5-d21b-4e9b-b793-6363471dff3c",
      "name": "Parse AI Analysis",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        480
      ],
      "parameters": {
        "jsCode": "// Parse GPT-4o JSON response and merge with intake data\nconst item = $input.first();\nconst raw = item.json.message?.content || item.json.choices?.[0]?.message?.content || '';\n\nlet analysis;\ntry {\n  const cleaned = raw.replace(/```json|```/g, '').trim();\n  analysis = JSON.parse(cleaned);\n} catch (e) {\n  analysis = {\n    comparisonTableHtml: '<p>Unable to parse vendor quotes.</p>',\n    recommendedVendor: 'Manual Review Required',\n    recommendedVendorRationale: raw,\n    riskFlags: ['AI parsing error - please review manually'],\n    executiveSummary: raw\n  };\n}\n\n// Build approval URL (replace with your actual n8n webhook URL after deployment)\nconst approvalBaseUrl = 'https://YOUR-N8N-INSTANCE/webhook/approve-rental';\nconst approvalUrl = `${approvalBaseUrl}?id=${item.json.breakdownId}&vendor=${encodeURIComponent(analysis.recommendedVendor)}&approved=true`;\nconst rejectUrl = `${approvalBaseUrl}?id=${item.json.breakdownId}&approved=false`;\n\nreturn [{\n  json: {\n    ...item.json,\n    analysis,\n    approvalUrl,\n    rejectUrl\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7768fe57-c763-4c2d-b436-f7c6eb9ea619",
      "name": "Email PM: Vendor Comparison + Approval Request",
      "type": "n8n-nodes-base.gmail",
      "notes": "Change sendTo to your actual PM email address",
      "position": [
        2912,
        480
      ],
      "parameters": {
        "sendTo": "pm@example.com",
        "message": "=<html>\n<body style=\"font-family: Arial, sans-serif; color: #333; max-width: 800px;\">\n  <h2 style=\"color: #2c3e50;\">Equipment Rental Approval Required</h2>\n  \n  <div style=\"background: #eaf4fb; padding: 15px; border-left: 4px solid #2980b9; margin-bottom: 20px;\">\n    <strong>\ud83d\udccb Breakdown Summary</strong><br/>\n    Machine: <strong>{{ $('Normalise Intake Data').item.json.machineType }}  </strong> | Site: <strong>   {{ $('Normalise Intake Data').item.json.siteName }} </strong> | Duration: <strong {{ $('Normalise Intake Data').item.json.durationDays }}day(s)</strong> | Urgency: <strong style=\"color:#c0392b;\">{{ $('Normalise Intake Data').item.json.urgency }} </strong><br/>\n    Reported by: {{ $('Normalise Intake Data').item.json.foremanName }} at{{ $('Normalise Intake Data').item.json.reportedAt }}\n  </div>\n  \n  <h3>\ud83d\udcca Vendor Comparison</h3>\n  {{ $json.analysis.comparisonTableHtml }}\n  \n  <div style=\"background: #eafaf1; padding: 15px; border-left: 4px solid #27ae60; margin: 20px 0;\">\n    <strong>\u2705 AI Recommendation: {{ $json.analysis.recommendedVendor }}</strong><br/>\n    {{ $json.analysis.recommendedVendorRationale }}\n  </div>\n  \n  {% if $json.analysis.riskFlags.length > 0 %}\n  <div style=\"background: #fef9e7; padding: 15px; border-left: 4px solid #f39c12; margin: 20px 0;\">\n    <strong>\u26a0\ufe0f Risk Flags:</strong>\n    <ul>{% for flag in $json.analysis.riskFlags %}<li>  {{ $json.analysis.riskFlags[0] }}</li>{% endfor %}</ul>\n  </div>\n  {% endif %}\n  \n  <p><strong>Executive Summary:</strong><br/>{{ $json.analysis.executiveSummary }}</p>\n  \n  <div style=\"margin: 30px 0;\">\n    <a href=\"{{ $json.approvalUrl }}\" style=\"background: #27ae60; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-size: 16px; margin-right: 15px;\">\u2705 APPROVE BOOKING</a>\n    <a href=\"{{ $json.rejectUrl }}\" style=\"background: #e74c3c; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; font-size: 16px;\">\u274c REJECT / MANUAL REVIEW</a>\n  </div>\n  \n  <p style=\"color: #888; font-size: 12px;\">This is an automated recommendation. Reference: {{ $json.breakdownId }}</p>\n</body>\n</html>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\u26a1 Approval Required: Equipment Rental for {{ $('Split Into Vendor Items').item.json.machineType }} |  {{ $('Split Into Vendor Items').item.json.siteName }}| Ref: {{ $('Split Into Vendor Items').item.json.breakdownId }}",
        "operation": "sendAndWait"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "fcd26c00-a664-4c49-9936-ae44ce5cc47f",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "position": [
        3312,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "approval-check",
              "operator": {
                "type": "boolean",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.data.approved }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f96a93b4-db53-494d-b0fb-fcabc24d822c",
      "name": "Send Booking Confirmation to Vendor",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3584,
        464
      ],
      "parameters": {
        "sendTo": "=vendor@example.com",
        "message": "=<html>\n<body style=\"font-family: Arial, sans-serif; color: #333;\">\n  <h2 style=\"color: #27ae60;\">\u2705 Rental Booking Confirmed</h2>\n  <p>Dear {{ $json.analysis.recommendedVendor }} Team,</p>\n  <p>We are pleased to confirm the following rental booking. Please proceed with delivery arrangements as per the details below.</p>\n  \n  <table style=\"border-collapse: collapse; width: 100%; margin: 20px 0;\">\n    <tr style=\"background: #f2f2f2;\"><th style=\"padding: 10px; border: 1px solid #ddd; text-align: left;\">Field</th><th style=\"padding: 10px; border: 1px solid #ddd; text-align: left;\">Details</th></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Booking Reference</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $('Split Into Vendor Items').item.json.breakdownId }} </td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Machine Required</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $('Split Into Vendor Items').item.json.machineType }} </td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Delivery Site</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $('Split Into Vendor Items').item.json.siteName }} </td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Rental Duration</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ $('Split Into Vendor Items').item.json.durationDays }} day(s)</td></tr>\n    <tr><td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Approved At</strong></td><td style=\"padding: 10px; border: 1px solid #ddd;\">{{ new Date().toISOString() }}</td></tr>\n  </table>\n  \n  <p>Please confirm receipt and provide your expected delivery time by return email.</p>\n  <p>Thank you for your prompt response.</p>\n  \n  <p style=\"color: #888; font-size: 12px;\">Reference: {{ $json.breakdownId }}</p>\n</body>\n</html>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\u2705 Booking Confirmed \u2013 {{ $('Split Into Vendor Items').item.json.machineType }} |  {{ $('Split Into Vendor Items').item.json.siteName }}| Ref: {{ $('Split Into Vendor Items').item.json.breakdownId }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "e248398a-fe62-48d0-b19d-22f61fd0ca2c",
      "name": "Notify Foreman via Telegram: Booking Confirmed",
      "type": "n8n-nodes-base.telegram",
      "notes": "Only fires if foreman submitted via Telegram and provided chat ID",
      "position": [
        3584,
        192
      ],
      "parameters": {
        "text": "=\u2705 *Rental Confirmed!*\n\nYour breakdown report ({{ $('Split Into Vendor Items').item.json.breakdownId }}) has been processed.\n\n\ud83c\udfd7 *Machine:* {{ $('Split Into Vendor Items').item.json.machineType }}\n\ud83d\udccd *Site:*{{ $('Split Into Vendor Items').item.json.siteName }}\n\ud83c\udfe2 *Vendor:* {{ $('Aggregate Vendor Quotes').item.json.availableQuotes[0].vendor }}\n\ud83d\udcc5 *Duration:*  {{ $('Split Into Vendor Items').item.json.durationDays }}day(s)\n\nConfirmation has been sent to the vendor. Delivery details will follow shortly.",
        "chatId": "={{ $('Telegram: Breakdown Report').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ea2ce7e2-a881-4127-8414-aa1aa5238aba",
      "name": "Email PM: Rejection Notice",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3584,
        736
      ],
      "parameters": {
        "sendTo": "pm@example.com",
        "message": "=<html>\n<body style=\"font-family: Arial, sans-serif; color: #333;\">\n  <h2 style=\"color: #e74c3c;\">Rental Booking Rejected \u2013 Manual Review Required</h2>\n  <p>The automated rental recommendation for <strong>{{ $json.machineType }}</strong> at <strong>{{ $json.siteName }}</strong> was rejected.</p>\n  <p><strong>Reference:</strong> {{ $json.breakdownId }}</p>\n  <p>Please contact vendors directly or re-run the workflow with updated parameters.</p>\n  <p style=\"color: #888; font-size: 12px;\">This is an automated notification.</p>\n</body>\n</html>",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\u2139\ufe0f Rental Request Rejected \u2013 Manual Review Required | {{ $json.breakdownId }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ec7b236a-7cae-4667-a9fc-962370917692",
      "name": "Azure OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "position": [
        2320,
        752
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {}
      },
      "credentials": {
        "azureOpenAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7cda3ba9-584a-4ad0-acb8-284e41daf79a",
      "name": "AI Agent: Analyse & Rank Vendor Quotes",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2400,
        480
      ],
      "parameters": {
        "text": "=You are a construction procurement analyst specialising in equipment rentals.\n\nBreakdown Details:\n- Machine Required: {{ $('Split Into Vendor Items').item.json.machineType }}\n- Site: {{ $('Split Into Vendor Items').item.json.siteName }}\n- Duration:{{ $('Split Into Vendor Items').item.json.durationDays }}day(s)\n- Urgency:{{ $('Split Into Vendor Items').item.json.urgency }}\n- Breakdown ID: {{ $('Split Into Vendor Items').item.json.breakdownId }}\n\nAvailable Vendor Quotes:\n{{ JSON.stringify($json.availableQuotes, null, 2) }}\n\nAnalyse the quotes and respond with ONLY valid JSON \u2014 no markdown, no explanation, no code fences:\n\n{\n  \"comparisonTableHtml\": \"<table style='border-collapse:collapse;width:100%'><tr style='background:#f2f2f2'><th style='padding:10px;border:1px solid #ddd'>Vendor</th><th style='padding:10px;border:1px solid #ddd'>Daily Rate</th><th style='padding:10px;border:1px solid #ddd'>Total Cost</th><th style='padding:10px;border:1px solid #ddd'>Delivery (hrs)</th><th style='padding:10px;border:1px solid #ddd'>Notes</th></tr>... rows here ...</table>\",\n  \"recommendedVendor\": \"Vendor Name Here\",\n  \"recommendedVendorRationale\": \"2-3 sentence justification covering price, delivery speed, and conditions\",\n  \"riskFlags\": [\"flag1 if any\", \"flag2 if any\"],\n  \"executiveSummary\": \"One paragraph for the Project Manager summarising the situation and recommended action\"\n}",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 2.1
    },
    {
      "id": "7fd0270e-97ce-4df0-bb5d-69ee4375fcd6",
      "name": "Wait for Vendor Reply Window",
      "type": "n8n-nodes-base.wait",
      "position": [
        1568,
        496
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "6c62507e-e6a3-49fe-91c3-4cc34689d862",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        1840,
        496
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "05adb9d2-ad13-40a3-822f-ee052cdcf4db",
              "operator": {
                "type": "boolean",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.data.approved }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "afbdc956-9330-466e-a318-eef5bcfc8b57",
      "name": "Wait for PM Approval Response",
      "type": "n8n-nodes-base.wait",
      "position": [
        3104,
        480
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "db50116c-babf-4197-bafa-4205e7ffcede",
      "name": "On Workflow Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        640,
        1216
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "bd7b1ee8-b725-46d8-8988-bd904ea77b36",
      "name": "Slack \u2013 Send Error Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        896,
        1216
      ],
      "parameters": {
        "text": "=error in the workflow",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0AN1UGL0RM",
          "cachedResultName": "all-n8n-automations"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "f979a636-d104-4dbd-aaec-7d75d1ce5b3c",
      "name": "Section: Error Handler",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        1056
      ],
      "parameters": {
        "color": 7,
        "width": 556,
        "height": 368,
        "content": "## \u26a0\ufe0f Error Handler\nCatches any failure in the workflow and posts a Slack alert with the error message, failing node name, and execution ID. Wire the error output of any critical node here to prevent silent failures going unnoticed."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "2259b8fb-3db8-40fb-a9c4-9c1b6990b406",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Aggregate Vendor Quotes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Notify Foreman via Telegram: Booking Confirmed",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Booking Confirmation to Vendor",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email PM: Rejection Notice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Workflow Error": {
      "main": [
        [
          {
            "node": "Slack \u2013 Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Analysis": {
      "main": [
        [
          {
            "node": "Email PM: Vendor Comparison + Approval Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalise Intake Data": {
      "main": [
        [
          {
            "node": "Split Into Vendor Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Vendor Quotes": {
      "main": [
        [
          {
            "node": "AI Agent: Analyse & Rank Vendor Quotes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Azure OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent: Analyse & Rank Vendor Quotes",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Vendor Items": {
      "main": [
        [
          {
            "node": "Send Vendor Quote Request Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram: Breakdown Report": {
      "main": [
        [
          {
            "node": "Normalise Intake Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Vendor Reply Window": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for PM Approval Response": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Vendor Quote Request Emails": {
      "main": [
        [
          {
            "node": "Wait for Vendor Reply Window",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent: Analyse & Rank Vendor Quotes": {
      "main": [
        [
          {
            "node": "Parse AI Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email PM: Vendor Comparison + Approval Request": {
      "main": [
        [
          {
            "node": "Wait for PM Approval Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}