AutomationFlowsAI & RAG › Generate Construction Bid Estimates with Azure Openai, Google Docs and Sheets

Generate Construction Bid Estimates with Azure Openai, Google Docs and Sheets

ByRahul Joshi @rahul08 on n8n.io

This workflow collects a construction Scope of Work PDF via an n8n form, uses Azure OpenAI to extract BOQ-style line items, prices them using a Google Sheets unit-rate database, generates a formatted Google Docs bid report, logs the estimate to Google Sheets, and emails the…

Event trigger★★★★☆ complexityAI-powered22 nodesLm Chat Azure Open AiAgentForm TriggerGoogle DocsGoogle SheetsGmailError TriggerSlack
AI & RAG Trigger: Event Nodes: 22 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Agent → Error Trigger 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": "JhgvCr1iu2hAJlnG",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Construction Bid Estimation & Cost Proposal Generator",
  "tags": [],
  "nodes": [
    {
      "id": "aa472e6a-67f7-49f5-a1dd-997dd7d28c70",
      "name": "\ud83d\udccb Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        5632
      ],
      "parameters": {
        "width": 608,
        "height": 580,
        "content": "## \ud83c\udfd7\ufe0f AI Bid Estimation \u2014 Scope of Work to Cost Report\n\n### How it works\nAn estimator uploads a Scope of Work PDF through a form. The AI reads the document, extracts structured line items (category, quantity, unit), and cross-references a historical unit rate database in Google Sheets. It then calculates per-item costs, generates a full bid report in Google Docs, logs the estimate to a BidLog sheet, and emails the completed estimate to the estimator \u2014 all in one automated pass.\n\n### Setup steps\n1. **Google Sheets** \u2014 Open the Unit Rates database spreadsheet. Populate the `UnitRates` sheet with your historical rates (`Description Keyword`, `Unit Rate (\u20b9)`, `Source / Project Ref`). The `BidLog` sheet captures all generated estimates automatically.\n2. **Azure OpenAI** \u2014 Connect your Azure OpenAI credential. The deployment name must match a GPT-4o-mini deployment in your Azure instance.\n3. **Google Docs** \u2014 Set the target folder ID in the `Google Docs \u2014 Create Bid Report` node where estimate documents should be saved.\n4. **Gmail** \u2014 Connect a Gmail OAuth2 credential for outbound estimate delivery.\n5. **Activate** the workflow and share the form URL with your estimation team.\n\n> \u26a0\ufe0f Quantities extracted by AI are estimates. Always verify before final client submission."
      },
      "typeVersion": 1
    },
    {
      "id": "098e6013-455f-4c60-b643-718620f9259d",
      "name": "Section \u2014 Intake",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        6400
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 472,
        "content": "## \ud83d\udce5 Intake & PDF Extraction\nCaptures project details and the uploaded SOW PDF via a form, then extracts raw text from the PDF so the AI can process it. Both the rate lookup and text extraction run in parallel from this trigger."
      },
      "typeVersion": 1
    },
    {
      "id": "95b5d8c1-dc4d-4c86-9126-4b24bd818563",
      "name": "Section \u2014 AI Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -96,
        6528
      ],
      "parameters": {
        "color": 7,
        "width": 756,
        "height": 568,
        "content": "## \ud83e\udd16 AI Extraction & Parsing\nSends extracted PDF text to GPT-4o-mini with a structured prompt. The model returns a JSON list of line items \u2014 category, code, description, unit, quantity, and notes. The Code node cleans the response and enriches each item with form metadata."
      },
      "typeVersion": 1
    },
    {
      "id": "d7e72414-37ed-4329-86e4-6d9373d211ef",
      "name": "Section \u2014 Costing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        6064
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 360,
        "content": "## \ud83d\udcb0 Rate Lookup & Cost Calculation\nMatches each AI-extracted line item against the historical unit rate database using keyword search. Falls back to category-level default rates (\u20b9) when no keyword match is found. Aggregates all priced items for summary generation."
      },
      "typeVersion": 1
    },
    {
      "id": "82544112-7327-4943-b375-03a7a21c1fa5",
      "name": "Section \u2014 Output",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        6256
      ],
      "parameters": {
        "color": 7,
        "width": 1328,
        "height": 744,
        "content": "## \ud83d\udcc4 Report Generation & Logging\nBuilds a formatted bid summary (category totals, grand total ex/inc GST). Creates a Google Doc with the full report, appends the estimate to the BidLog sheet for audit trail, then emails the estimator with a direct link to the document."
      },
      "typeVersion": 1
    },
    {
      "id": "0ea6cf06-d449-4b10-8128-9b3f2ad7d1e2",
      "name": "Credentials Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1824,
        7072
      ],
      "parameters": {
        "color": 3,
        "width": 304,
        "height": 224,
        "content": "## \ud83d\udd10 Credentials & Security\nUse OAuth2 for Google Docs, Google Sheets, and Gmail. Use your Azure OpenAI API credential for the chat model node. Never hardcode API keys or personal emails in node parameters \u2014 use n8n credentials manager."
      },
      "typeVersion": 1
    },
    {
      "id": "2f1f6a59-ff73-4173-8854-62b4b882f518",
      "name": "Azure OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "position": [
        192,
        6928
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {}
      },
      "credentials": {
        "azureOpenAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b3194be8-ffb6-44e6-93fd-bf573f882b2e",
      "name": "\ud83e\udd16 AI Agent \u2013 Extract Line Items",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        128,
        6704
      ],
      "parameters": {
        "text": "=You are an expert construction cost estimator. Your job is to extract structured line items from a scope of work document. Always respond with ONLY valid JSON \u2014 no markdown, no explanation, no code fences.\n\nFor each line item extract:\n- category: one of [Civil, MEP, Finishes, Structural, External Works, Preliminaries]\n- item_code: a short unique code like CIV-001, MEP-002\n- description: clear description of work item\n- unit: unit of measurement (m\u00b2, m\u00b3, lm, nr, ls, kg)\n- quantity: estimated numeric quantity (estimate if not explicit)\n- notes: any special conditions or assumptions\n\nRespond ONLY with this exact JSON structure, no other text:\n{\n  \"project_summary\": \"brief one-liner about the project\",\n  \"line_items\": [\n    {\n      \"category\": \"Civil\",\n      \"item_code\": \"CIV-001\",\n      \"description\": \"Excavation for foundations\",\n      \"unit\": \"m\u00b3\",\n      \"quantity\": 150,\n      \"notes\": \"Assumed medium soil conditions\"\n    }\n  ]\n}\n\nProject: {{ $('Form \u2014 Upload SOW').item.json['Project Name'] }}\nClient: {{ $('Form \u2014 Upload SOW').item.json['Client Name'] }}\n\nDocument Content:\n{{ $json.text }}",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 2.1
    },
    {
      "id": "61735709-7361-4e33-9f2a-d7e17950d43c",
      "name": "Form \u2014 Upload SOW",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -400,
        6560
      ],
      "parameters": {
        "options": {},
        "formTitle": "AI Bid Estimation \u2014 Upload Scope of Work",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Project Name",
              "requiredField": true
            },
            {
              "fieldLabel": "Client Name",
              "requiredField": true
            },
            {
              "fieldType": "email",
              "fieldLabel": "Estimator Email",
              "requiredField": true
            },
            {
              "fieldType": "file",
              "fieldLabel": "Scope of Work PDF",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            }
          ]
        },
        "responseMode": "lastNode",
        "formDescription": "Upload your Scope of Work PDF. The AI will extract all line items and generate a cost estimate automatically."
      },
      "typeVersion": 2.2
    },
    {
      "id": "c45523de-bf2c-4afd-9eec-df669764b585",
      "name": "Extract from File",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -96,
        6704
      ],
      "parameters": {
        "options": {},
        "operation": "pdf",
        "binaryPropertyName": "Scope_of_Work_PDF"
      },
      "typeVersion": 1
    },
    {
      "id": "3b26e1c1-1093-498f-b48b-a015bca96d72",
      "name": "Parse Extracted Data",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        6704
      ],
      "parameters": {
        "jsCode": "const rawText = $input.first().json.output;\n\nlet parsed;\n\ntry {\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch (e) {\n  throw new Error(\n    'GPT-4o did not return valid JSON. Raw: ' +\n    rawText.substring(0, 300)\n  );\n}\n\nconst formData = {\n  projectName: $('Form \u2014 Upload SOW').first().json['Project Name'],\n  clientName: $('Form \u2014 Upload SOW').first().json['Client Name'],\n  estimatorEmail: $('Form \u2014 Upload SOW').first().json['Estimator Email'],\n  projectTitle: parsed.project_title || 'Untitled Project'\n};\n\nreturn parsed.line_items.map(item => ({\n  json: {\n    ...item,\n    ...formData\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "950ac99a-ea46-4d1e-a15a-77ec548ee991",
      "name": "Calculate Cost Per Item",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        6576
      ],
      "parameters": {
        "jsCode": "// BOQ items extracted from AI\nconst boqItems = $('Parse Extracted Data').all();\n\n// Historical rate database\nconst rateRows = $('Google Sheets \u2014 Rate Lookup1').all();\n\nconst results = [];\n\nfor (const item of boqItems) {\n\n  const description = (item.json.description || '').toLowerCase();\n\n  // Find matching rate row\n  const match = rateRows.find(rate => {\n    const keyword = (\n      rate.json['Description Keyword'] || ''\n    ).toLowerCase();\n\n    return description.includes(keyword);\n  });\n\n  let unitRate = 0;\n  let source = 'Fallback Rate';\n\n  if (match) {\n    unitRate = Number(match.json['Unit Rate (\u20b9)']) || 0;\n    source = match.json['Source / Project Ref'] || 'Historical Database';\n  }\n\n  // Fallback by category\n  if (!unitRate) {\n\n    const category =\n      (item.json.category || '').toLowerCase();\n\n    if (category.includes('civil')) unitRate = 4500;\n    else if (category.includes('structural')) unitRate = 6500;\n    else if (category.includes('mep')) unitRate = 3200;\n    else if (category.includes('finishes')) unitRate = 2800;\n    else if (category.includes('external')) unitRate = 1850;\n    else if (category.includes('preliminaries')) unitRate = 150000;\n  }\n\n  const quantity =\n    Number(item.json.quantity) || 0;\n\n  const totalCost =\n    quantity * unitRate;\n\n  results.push({\n    json: {\n      ...item.json,\n      unit_rate_inr: unitRate,\n      total_cost_inr: totalCost,\n      rate_source: source,\n      remarks: match\n        ? ''\n        : 'Fallback category rate used'\n    }\n  });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "4fe99b19-6239-40fb-837e-29354eb21eba",
      "name": "Aggregate All Line Items",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        944,
        6576
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "d28be835-219c-4494-a5e7-90a7a5c000a1",
      "name": "Build Bid Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        6576
      ],
      "parameters": {
        "jsCode": "// Build Bid Summary\n\nconst allItems = $input.first().json;\n\n// Aggregate node output\nconst lineItems = allItems.data || [];\n\n// Calculate category totals\nconst categoryTotals = {};\n\nlineItems.forEach(item => {\n  const cat = item.category || 'Other';\n\n  categoryTotals[cat] =\n    (categoryTotals[cat] || 0) +\n    (Number(item.total_cost_inr) || 0);\n});\n\n// Grand total\nconst grandTotal = Object.values(categoryTotals)\n  .reduce((a, b) => a + b, 0);\n\n// Formatters\nconst fmt = (n) =>\n  new Intl.NumberFormat('en-IN', {\n    style: 'currency',\n    currency: 'INR',\n    maximumFractionDigits: 0,\n  }).format(n || 0);\n\nconst fmtNum = (n) =>\n  new Intl.NumberFormat('en-IN')\n    .format(n || 0);\n\n// Get project metadata\nconst firstItem =\n  lineItems.find(i => i.projectName) || {};\n\nconst projectName =\n  firstItem.projectName || 'Unknown Project';\n\nconst clientName =\n  firstItem.clientName || 'Unknown Client';\n\nconst estimatorEmail =\n  firstItem.estimatorEmail || '';\n\nconst projectTitle =\n  firstItem.projectTitle || '';\n\nconst timestamp = new Date()\n  .toLocaleString('en-IN', {\n    timeZone: 'Asia/Kolkata'\n  });\n\n// Build report body\nlet docBody = '';\n\ndocBody += 'BID ESTIMATION SUMMARY\\n';\ndocBody += '='.repeat(80) + '\\n\\n';\n\ndocBody += `Project      : ${projectName}\\n`;\ndocBody += `Client       : ${clientName}\\n`;\ndocBody += `Reference    : ${projectTitle}\\n`;\ndocBody += `Generated On : ${timestamp}\\n`;\ndocBody += `Estimator    : ${estimatorEmail}\\n\\n`;\n\ndocBody += '='.repeat(80) + '\\n';\n\n// Group by category\nObject.keys(categoryTotals).forEach(cat => {\n\n  docBody += `\\n\\n${cat.toUpperCase()}\\n`;\n  docBody += '-'.repeat(80) + '\\n';\n\n  const items = lineItems.filter(\n    i => (i.category || 'Other') === cat\n  );\n\n  items.forEach(item => {\n\n    const itemCode =\n      item.item_code || 'N/A';\n\n    const description =\n      (item.description || 'Unknown Item')\n      .substring(0, 45);\n\n    const unit =\n      item.unit || '-';\n\n    const qty =\n      fmtNum(item.quantity);\n\n    const rate =\n      fmt(item.unit_rate_inr);\n\n    const total =\n      fmt(item.total_cost_inr);\n\n    docBody +=\n      `${itemCode} | ` +\n      `${description} | ` +\n      `${unit} | ` +\n      `${qty} | ` +\n      `${rate} | ` +\n      `${total}\\n`;\n  });\n\n  docBody += '\\n';\n  docBody += `CATEGORY TOTAL: ${fmt(categoryTotals[cat])}\\n`;\n});\n\ndocBody += '\\n';\ndocBody += '='.repeat(80) + '\\n';\n\ndocBody += `GRAND TOTAL (EX GST): ${fmt(grandTotal)}\\n`;\ndocBody += `GST @ 18%: ${fmt(grandTotal * 0.18)}\\n`;\ndocBody += `GRAND TOTAL (INC GST): ${fmt(grandTotal * 1.18)}\\n`;\n\ndocBody += '\\n';\ndocBody += '='.repeat(80) + '\\n\\n';\n\ndocBody += 'NOTES:\\n';\ndocBody += '- Rates sourced from historical database.\\n';\ndocBody += '- Fallback rates applied where match unavailable.\\n';\ndocBody += '- Quantities must be verified before final submission.\\n';\ndocBody += '- AI-generated estimate for review purposes.\\n';\n\nreturn [\n  {\n    json: {\n      docBody,\n      lineItems,\n      categoryTotals,\n      grandTotal,\n      grandTotalWithGST:\n        grandTotal * 1.18,\n      lineItemCount:\n        lineItems.length,\n      projectName,\n      clientName,\n      estimatorEmail,\n      timestamp,\n      docTitle:\n        `Bid Estimate - ${projectName}`\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "3d6ece14-7aa8-4829-b6c2-3300aedd68c4",
      "name": "Google Docs \u2014 Create Bid Report",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        1392,
        6480
      ],
      "parameters": {
        "title": "={{ $json.docTitle }}",
        "folderId": "your-google-drive-folder-id"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "910a3a36-c90f-4768-a797-7918dce5a04f",
      "name": "Google Sheets \u2014 Log Bid",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1504,
        6832
      ],
      "parameters": {
        "columns": {
          "value": {
            "\ud83d\udccb  BID LOG  \u2014  All Automated Estimates Generated": "={{ $json.docBody }}"
          },
          "schema": [
            {
              "id": "\ud83d\udccb  BID LOG  \u2014  All Automated Estimates Generated",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "\ud83d\udccb  BID LOG  \u2014  All Automated Estimates Generated",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "\ud83d\udccb  BID LOG  \u2014  All Automated Estimates Generated"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 934544741,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/your-google-sheets-document-id/edit#gid=934544741",
          "cachedResultName": "BidLog"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "your-google-sheets-document-id",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/your-google-sheets-document-id/edit",
          "cachedResultName": "Unit rates database"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "b3e59acb-ec6b-4ef3-968f-1d27902f8494",
      "name": "Gmail \u2014 Send Estimate to Estimator",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1840,
        6576
      ],
      "parameters": {
        "sendTo": "={{ $('Build Bid Summary').first().json.estimatorEmail }}",
        "message": "=<div style=\"font-family: Arial, sans-serif; max-width: 600px;\">\n<h2 style=\"color: #1a56db;\">\ud83d\udccb Bid Estimate Ready for Review</h2>\n<p>Hi Estimator,</p>\n<p>Your AI-powered bid estimate has been generated and is ready for review.</p>\n<table style=\"width:100%; border-collapse:collapse; margin:16px 0;\">\n  <tr style=\"background:#f3f4f6;\">\n    <td style=\"padding:8px 12px; font-weight:bold;\">Project</td>\n    <td style=\"padding:8px 12px;\">{{ $('Build Bid Summary').first().json.projectName }}</td>\n  </tr>\n  <tr>\n    <td style=\"padding:8px 12px; font-weight:bold;\">Client</td>\n    <td style=\"padding:8px 12px;\">{{ $('Build Bid Summary').first().json.clientName }}</td>\n  </tr>\n  <tr style=\"background:#f3f4f6;\">\n    <td style=\"padding:8px 12px; font-weight:bold;\">Line Items</td>\n    <td style=\"padding:8px 12px;\">{{ $('Build Bid Summary').first().json.lineItemCount }}</td>\n  </tr>\n  <tr>\n    <td style=\"padding:8px 12px; font-weight:bold;\">Estimated Total (excl. GST)</td>\n    <td style=\"padding:8px 12px; color:#059669; font-size:16px;\"><strong>\u20b9{{ $('Build Bid Summary').first().json.grandTotal.toLocaleString('en-IN') }}</strong></td>\n  </tr>\n  <tr style=\"background:#f3f4f6;\">\n    <td style=\"padding:8px 12px; font-weight:bold;\">Estimated Total (incl. GST 18%)</td>\n    <td style=\"padding:8px 12px; color:#1a56db; font-size:16px;\"><strong>\u20b9{{ $('Build Bid Summary').first().json.grandTotalWithGST.toLocaleString('en-IN') }}</strong></td>\n  </tr>\n</table>\n<p>\n<a href=\"{{ $('Google Docs \u2014 Create Bid Report').first().json.webViewLink }}\">\n    Open Full Bid Report\n</a>\n</p>\n<hr style=\"border:none; border-top:1px solid #e5e7eb; margin:20px 0;\">\n<p style=\"color:#6b7280; font-size:12px;\">This estimate was auto-generated by the AI Bid Estimation System using historical unit rates. Please verify quantities and rates before submitting to client. All rates are inclusive of material, labour, OH&P.</p>\n</div>",
        "options": {},
        "subject": "=Bid Estimate Ready \u2014 {{ $('Build Bid Summary').first().json.projectName }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "371c25bf-f863-43ee-83a3-8e044d3ab6ee",
      "name": "Google Sheets \u2014 Rate Lookup1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        176,
        6240
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": []
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 295316342,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/your-google-sheets-document-id/edit#gid=295316342",
          "cachedResultName": "UnitRates"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "your-google-sheets-document-id",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/your-google-sheets-document-id/edit",
          "cachedResultName": "Unit rates database"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "executeOnce": true,
      "typeVersion": 4.5
    },
    {
      "id": "7fe913f8-3547-4647-b33b-3a8fd838f0a3",
      "name": "Google Docs \u2014 Write Report Content",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        1616,
        6480
      ],
      "parameters": {
        "actionsUi": {
          "actionFields": [
            {
              "text": "={{ $('Build Bid Summary').item.json.docBody }}",
              "action": "insert"
            }
          ]
        },
        "operation": "update",
        "documentURL": "={{ $json.id }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8953788d-8156-4d18-95e8-85e6467761da",
      "name": "On Workflow Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -592,
        7120
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "ac093cda-ae99-4875-8e5c-e551a93fd225",
      "name": "Slack \u2013 Send Error Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        -336,
        7120
      ],
      "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": "1e1b8611-aee7-4291-89e0-994cb6dd9d74",
      "name": "Section: Error Handler",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -688,
        6960
      ],
      "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": "0014825d-5397-4077-ba49-16f840e7bff9",
  "connections": {
    "Build Bid Summary": {
      "main": [
        [
          {
            "node": "Google Docs \u2014 Create Bid Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Google Sheets \u2014 Log Bid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from File": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Agent \u2013 Extract Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Workflow Error": {
      "main": [
        [
          {
            "node": "Slack \u2013 Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Form \u2014 Upload SOW": {
      "main": [
        [
          {
            "node": "Extract from File",
            "type": "main",
            "index": 0
          },
          {
            "node": "Google Sheets \u2014 Rate Lookup1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Extracted Data": {
      "main": [
        [
          {
            "node": "Calculate Cost Per Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Azure OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "\ud83e\udd16 AI Agent \u2013 Extract Line Items",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Cost Per Item": {
      "main": [
        [
          {
            "node": "Aggregate All Line Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate All Line Items": {
      "main": [
        [
          {
            "node": "Build Bid Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2014 Log Bid": {
      "main": [
        [
          {
            "node": "Gmail \u2014 Send Estimate to Estimator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets \u2014 Rate Lookup1": {
      "main": [
        [
          {
            "node": "Calculate Cost Per Item",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Docs \u2014 Create Bid Report": {
      "main": [
        [
          {
            "node": "Google Docs \u2014 Write Report Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Docs \u2014 Write Report Content": {
      "main": [
        [
          {
            "node": "Gmail \u2014 Send Estimate to Estimator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 AI Agent \u2013 Extract Line Items": {
      "main": [
        [
          {
            "node": "Parse Extracted Data",
            "type": "main",
            "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 collects a construction Scope of Work PDF via an n8n form, uses Azure OpenAI to extract BOQ-style line items, prices them using a Google Sheets unit-rate database, generates a formatted Google Docs bid report, logs the estimate to Google Sheets, and emails the…

Source: https://n8n.io/workflows/16426/ — 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 contains community nodes that are only compatible with the self-hosted version of n8n.

Google Sheets, Form Trigger, Output Parser Structured +7
AI & RAG

Transform your manual hiring process into an intelligent evaluation system that saves 15-20 minutes per candidate! This workflow automates the entire candidate assessment pipeline - from CSV/XLSX uplo

Form Trigger, Google Sheets, Google Drive +8
AI & RAG

This workflow automates end-to-end validation, assessment, and reporting of n8n workflow JSON templates using Google Drive, Azure OpenAI GPT-4o, Gmail, and Slack. It retrieves workflows from a Drive f

Memory Buffer Window, Lm Chat Azure Open Ai, Output Parser Structured +5
AI & RAG

This workflow is an AI-powered virtual cinematography and previs generation pipeline designed for film and VFX production. It transforms a director’s shot description into multiple camera choreography

Agent, Lm Chat Azure Open Ai, HTTP Request +7
AI & RAG

Automatically capture customer onboarding help requests from Typeform, log them in Google Sheets, validate email addresses, and send a professional HTML welcome email via Gmail. Ensures smooth onboard

Typeform Trigger, Google Sheets, Gmail +6