AutomationFlowsAI & RAG › Evaluate Vendor Proposal Pdfs with Openai, Gmail, and Google Sheets

Evaluate Vendor Proposal Pdfs with Openai, Gmail, and Google Sheets

ByisaWOW @isawow on n8n.io

This workflow collects vendor proposal PDFs via an n8n Form, extracts text from the PDF, uses an OpenAI ChatGPT model to summarize and flag risks, logs the evaluation to Google Sheets, and sends an approval or review-needed notification through Gmail. Receives a submission from…

Event trigger★★★★☆ complexityAI-powered15 nodesForm TriggerAgentOpenAI ChatGoogle SheetsGmail
AI & RAG Trigger: Event Nodes: 15 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Agent → Form 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": "7hjkq2Ykm6dPKDmb",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Vendor Proposal PDF Evaluator \u2014 Form Upload + ChatGPT AI + Gmail + Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "254a5fd1-c7cf-4ef7-8410-2ef3b079e91a",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1568,
        -272
      ],
      "parameters": {
        "color": 4,
        "width": 524,
        "height": 1284,
        "content": "## Vendor Proposal PDF Evaluator \u2014 Form Upload + ChatGPT AI + Gmail + Google Sheets\n\nFor procurement and operations teams who want every vendor or partner proposal screened automatically before a human reads it. Upload a proposal PDF via a simple form. A ChatGPT AI model reads the document, extracts the vendor name, category, summary, and key points, identifies risks or missing information, and makes an automatic decision:\n- If risks are found, a Gmail alert goes to your review team and the row is logged in Sheets as REVIEW NEEDED\n- If no risks are found, a Gmail approval confirmation is sent and the row is logged in Sheets as APPROVED\n\n## How it works\n- **1. Form \u2014 Upload Vendor Proposal PDF** collects your name, email, the PDF, and optional notes\n- **2. Extract From File \u2014 Parse PDF Text** converts the uploaded PDF binary to text\n- **3. Code \u2014 Clean PDF Text and Prep Context** cleans the text, caps its length, and carries the submitter details forward\n- **4. AI Agent \u2014 Evaluate Proposal** uses a ChatGPT AI model to return structured JSON with summary, key points, and risks\n- **OpenAI Chat Model** the language model attached to the AI Agent\n- **5. Code \u2014 Parse AI Output and Risk Flag** parses the JSON, builds readable strings, and computes a risk flag\n- **6. IF \u2014 Risks Found?** routes to the risk path or the approved path\n- **7a / 7b. Sheets \u2014 Log Proposal** logs the evaluation on either branch\n- **8a. Gmail \u2014 Risk Alert to Review Team** emails the review team when risks are found\n- **8b. Gmail \u2014 Auto-Approval Notification** emails the approver when the proposal is clean\n\n## Set up steps\n1. In **OpenAI Chat Model** \u2014 connect your OpenAI API credential and pick any current ChatGPT model from the dropdown\n2. In both **7a. Sheets** and **7b. Sheets** \u2014 connect Google Sheets OAuth2 and replace `YOUR_PROPOSAL_LOG_SHEET_ID`. Create a tab named Evaluations with these columns: Submission Date, Vendor Name, Category, Summary, Risk Flagged, Risk Count, Key Points, Risks, Decision, Submitted By\n3. In **8a. Gmail** \u2014 connect Gmail OAuth2 and replace `YOUR_REVIEW_TEAM_EMAIL` (risk alert recipient)\n4. In **8b. Gmail** \u2014 replace `YOUR_APPROVER_EMAIL` (approval recipient). Both can be the same address\n5. Activate the workflow, open the Form URL, upload a proposal PDF, and submit. The decision is logged and emailed automatically"
      },
      "typeVersion": 1
    },
    {
      "id": "a53d430e-8488-4ec9-9c79-b1be06d79616",
      "name": "Section \u2014 PDF Upload and Parse",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1024,
        16
      ],
      "parameters": {
        "color": 5,
        "width": 836,
        "height": 516,
        "content": "## PDF Upload and Parse\nUser uploads a proposal PDF directly. Extract From File converts the binary PDF to text, and Code cleans it and prepares context \u2014 no URL download or file hosting needed."
      },
      "typeVersion": 1
    },
    {
      "id": "81772256-faec-4b40-bd02-30c6327bc266",
      "name": "Section \u2014 AI Evaluation and Risk Flag",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        -80
      ],
      "parameters": {
        "color": 6,
        "width": 340,
        "height": 676,
        "content": "## AI Evaluation and Risk Flag\nThe ChatGPT AI model reads the cleaned PDF text and returns structured JSON. Code parses it safely and computes a risk_flagged boolean used for routing."
      },
      "typeVersion": 1
    },
    {
      "id": "dbf420a9-6dd0-475c-b35b-c8ab8daa6cd7",
      "name": "Section \u2014 Risk Check, Log, and Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        288,
        -80
      ],
      "parameters": {
        "color": 4,
        "width": 1124,
        "height": 692,
        "content": "## Risk Check, Log, and Email\nIF checks the risk flag. Both branches log to Sheets. The risk path emails the review team; the clean path emails an approval. Each submission produces exactly one outcome."
      },
      "typeVersion": 1
    },
    {
      "id": "7dd015c9-2cd8-480e-aed0-2b74127786bf",
      "name": "1. Form \u2014 Upload Vendor Proposal PDF",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -928,
        224
      ],
      "parameters": {
        "options": {
          "path": "vendor-proposal-evaluator",
          "buttonLabel": "Evaluate Proposal",
          "appendAttribution": false
        },
        "formTitle": "Vendor Proposal Evaluator",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Your Name",
              "placeholder": "e.g. Rahul Kumar",
              "requiredField": true
            },
            {
              "fieldLabel": "Your Email",
              "placeholder": "user@example.com",
              "requiredField": true
            },
            {
              "fieldType": "file",
              "fieldLabel": "Vendor Proposal PDF",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".pdf"
            },
            {
              "fieldLabel": "Notes for Evaluator",
              "placeholder": "Optional \u2014 any context about this vendor or why you are submitting this proposal"
            }
          ]
        },
        "formDescription": "Upload a vendor or partner proposal PDF. AI will evaluate it for key information and risks, and send the decision to your team by email."
      },
      "typeVersion": 2.2
    },
    {
      "id": "95d0db9c-ce8a-418b-acc5-8e11b5bf94e7",
      "name": "2. Extract From File \u2014 Parse PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -688,
        224
      ],
      "parameters": {
        "options": {},
        "operation": "pdf"
      },
      "typeVersion": 1.1
    },
    {
      "id": "fc9f6aa4-4552-4fe2-a07d-b7ab73e850c4",
      "name": "3. Code \u2014 Clean PDF Text and Prep Context",
      "type": "n8n-nodes-base.code",
      "position": [
        -448,
        224
      ],
      "parameters": {
        "jsCode": "const pdfText  = $json.text || '';\nconst formData = $('1. Form \u2014 Upload Vendor Proposal PDF').first().json;\n\nconst submitter      = (formData['Your Name']  || '').trim();\nconst submitterEmail = (formData['Your Email'] || '').trim();\nconst notes          = (formData['Notes for Evaluator'] || '').trim();\nconst submittedAt    = new Date().toISOString().split('T')[0];\n\nif (!pdfText || pdfText.trim().length < 50) {\n  throw new Error(\n    'PDF text extraction failed or PDF is too short. ' +\n    'Make sure the PDF contains selectable text (not a scanned image).' +\n    ' Scanned PDFs require OCR pre-processing.'\n  );\n}\n\nlet cleanText = pdfText\n  .replace(/\\n\\s*\\n\\s*\\n/g, '\\n\\n')\n  .replace(/^\\d+\\s*$/gm, '')\n  .replace(/[ \\t]{3,}/g, '  ')\n  .trim()\n  .substring(0, 6000);\n\nreturn [{\n  json: {\n    cleanText,\n    submitter,\n    submitterEmail,\n    notes,\n    submittedAt\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "12bec350-fc1a-4185-8f49-184fba3b14c6",
      "name": "4. AI Agent \u2014 Evaluate Proposal",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -48,
        224
      ],
      "parameters": {
        "text": "=Evaluate the following vendor proposal text.\n\nSubmitter Notes: {{ $json.notes || 'None provided' }}\n\nProposal Text:\n{{ $json.cleanText }}",
        "options": {
          "systemMessage": "You are an expert vendor proposal evaluator and business analyst. Read the proposal text carefully and extract all relevant information.\n\nReturn ONLY valid JSON \u2014 no markdown, no code fences, no explanation text.\n\nReturn this exact structure:\n{\n  \"vendor_name\": \"<name of the vendor or company submitting the proposal \u2014 string>\",\n  \"category\": \"<category or domain of the proposal \u2014 e.g. Software Development, Marketing Services, Cloud Infrastructure \u2014 string>\",\n  \"summary\": \"<2 to 3 sentence objective summary of what the proposal is offering and the main value proposition \u2014 string>\",\n  \"key_points\": [\"<important highlight 1>\", \"<important highlight 2>\", \"<important highlight 3>\"],\n  \"risks_or_concerns\": [\"<risk or concern 1 \u2014 be specific>\", \"<risk or concern 2>\"]\n}\n\nRisk detection rules \u2014 flag any of these as a risk:\n- No pricing or cost information mentioned\n- No timeline or delivery schedule\n- No contact information or company registration details\n- Vague deliverables or unclear scope of work\n- Missing SLA or support terms for software/infrastructure proposals\n- Unrealistic claims without evidence or case studies\n- Legal or compliance concerns\n- Missing vendor credentials or past work references\n\nIf no genuine risks exist, return an empty array for risks_or_concerns.\nIf some information cannot be found, return empty string or empty array \u2014 never omit the key."
        },
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "a226590c-1c7b-4262-b35c-4ba62ae94bbf",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -48,
        432
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "8bf09543-f745-4a8c-b77f-0ceed03d1d54",
      "name": "5. Code \u2014 Parse AI Output and Risk Flag",
      "type": "n8n-nodes-base.code",
      "position": [
        368,
        224
      ],
      "parameters": {
        "jsCode": "const raw  = $input.first().json.output || '';\nconst meta = $('3. Code \u2014 Clean PDF Text and Prep Context').first().json;\n\nlet proposal = {};\ntry {\n  const cleaned = raw.trim()\n    .replace(/^```json\\s*/i, '')\n    .replace(/^```\\s*/i, '')\n    .replace(/```$/i, '')\n    .trim();\n  proposal = JSON.parse(cleaned);\n} catch (e) {\n  proposal = {\n    vendor_name:       'Unknown Vendor',\n    category:          'Unknown',\n    summary:           'AI evaluation failed to parse. Raw output: ' + raw.substring(0, 200),\n    key_points:        [],\n    risks_or_concerns: ['AI evaluation output could not be parsed \u2014 manual review required']\n  };\n}\n\nconst risks     = Array.isArray(proposal.risks_or_concerns) ? proposal.risks_or_concerns : [];\nconst keyPoints = Array.isArray(proposal.key_points) ? proposal.key_points : [];\nconst riskFlagged = risks.length > 0;\n\nconst risksStr     = risks.length     > 0 ? risks.map((r, i) => (i+1) + '. ' + r).join('\\n') : 'None identified';\nconst keyPointsStr = keyPoints.length > 0 ? keyPoints.map((k, i) => (i+1) + '. ' + k).join('\\n') : 'None extracted';\n\nreturn [{\n  json: {\n    vendorName:     proposal.vendor_name || 'Unknown Vendor',\n    category:       proposal.category    || 'Unknown',\n    summary:        proposal.summary     || '',\n    keyPoints,\n    risks,\n    keyPointsStr,\n    risksStr,\n    riskFlagged,\n    riskCount:      risks.length,\n    decision:       riskFlagged ? 'REVIEW NEEDED' : 'APPROVED',\n    submitter:      meta.submitter,\n    submitterEmail: meta.submitterEmail,\n    notes:          meta.notes,\n    submittedAt:    meta.submittedAt\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "83e78071-03e2-4be0-a4cc-f3b23949bd86",
      "name": "6. IF \u2014 Risks Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        608,
        224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "risk-check",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.riskFlagged }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "aebbbf66-5792-4e6e-ba19-08d754788477",
      "name": "7a. Sheets \u2014 Log Risky Proposal",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        848,
        64
      ],
      "parameters": {
        "columns": {
          "value": {
            "Risks": "={{ $json.risksStr }}",
            "Summary": "={{ $json.summary }}",
            "Category": "={{ $json.category }}",
            "Decision": "REVIEW NEEDED",
            "Key Points": "={{ $json.keyPointsStr }}",
            "Risk Count": "={{ $json.riskCount }}",
            "Vendor Name": "={{ $json.vendorName }}",
            "Risk Flagged": "YES",
            "Submitted By": "={{ $json.submitter }}",
            "Submission Date": "={{ $json.submittedAt }}"
          },
          "schema": [
            {
              "id": "Submission Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Submission Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vendor Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Vendor Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk Flagged",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risk Flagged",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk Count",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risk Count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Key Points",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Key Points",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risks",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risks",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Decision",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Decision",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Submitted By",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Submitted By",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Evaluations"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_PROPOSAL_LOG_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "a30401fd-1611-426e-9be2-e2ddf5eadcef",
      "name": "7b. Sheets \u2014 Log Approved Proposal",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        848,
        384
      ],
      "parameters": {
        "columns": {
          "value": {
            "Risks": "None",
            "Summary": "={{ $json.summary }}",
            "Category": "={{ $json.category }}",
            "Decision": "APPROVED",
            "Key Points": "={{ $json.keyPointsStr }}",
            "Risk Count": "0",
            "Vendor Name": "={{ $json.vendorName }}",
            "Risk Flagged": "NO",
            "Submitted By": "={{ $json.submitter }}",
            "Submission Date": "={{ $json.submittedAt }}"
          },
          "schema": [
            {
              "id": "Submission Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Submission Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Vendor Name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Vendor Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk Flagged",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risk Flagged",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk Count",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risk Count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Key Points",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Key Points",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risks",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risks",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Decision",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Decision",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Submitted By",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Submitted By",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Evaluations"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_PROPOSAL_LOG_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "a3eb20ab-0164-4afc-b97f-f5ada106e02b",
      "name": "8a. Gmail \u2014 Risk Alert to Review Team",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1088,
        64
      ],
      "parameters": {
        "sendTo": "YOUR_REVIEW_TEAM_EMAIL",
        "message": "=<div style=\"font-family:Arial,sans-serif;max-width:680px;margin:0 auto;background:#f5f5f5;\">\n\n  <div style=\"background:#c62828;padding:24px 28px;border-radius:8px 8px 0 0;\">\n    <h1 style=\"color:#fff;font-size:20px;margin:0;\">Proposal Review Required</h1>\n    <p style=\"color:rgba(255,255,255,0.85);font-size:13px;margin:6px 0 0;\">Submitted by {{ $json.submitter }} on {{ $json.submittedAt }}</p>\n  </div>\n\n  <div style=\"background:#b71c1c;padding:10px 24px;display:flex;gap:20px;flex-wrap:wrap;\">\n    <span style=\"color:#fff;font-size:13px;\">{{ $json.vendorName }}</span>\n    <span style=\"color:#fff;font-size:13px;\">{{ $json.category }}</span>\n    <span style=\"color:#ffcdd2;font-size:13px;font-weight:700;\">{{ $json.riskCount }} risk(s) identified</span>\n  </div>\n\n  <div style=\"background:#fff;padding:20px 24px;\">\n\n    <div style=\"background:#f0f4ff;border-left:4px solid #1565c0;padding:14px;border-radius:0 6px 6px 0;margin-bottom:18px;\">\n      <h2 style=\"margin:0 0 8px;font-size:14px;color:#1a1a2e;\">Proposal Summary</h2>\n      <p style=\"margin:0;font-size:14px;color:#333;line-height:1.6;\">{{ $json.summary }}</p>\n    </div>\n\n    <h2 style=\"font-size:14px;color:#1a1a2e;border-bottom:2px solid #eee;padding-bottom:6px;\">Key Points</h2>\n    <pre style=\"font-size:13px;color:#333;line-height:1.8;white-space:pre-wrap;background:#f9f9f9;padding:12px;border-radius:6px;margin-bottom:16px;\">{{ $json.keyPointsStr }}</pre>\n\n    <div style=\"background:#fff8e1;border-left:4px solid #f57f17;padding:14px;border-radius:0 6px 6px 0;margin-bottom:16px;\">\n      <h2 style=\"margin:0 0 10px;font-size:14px;color:#e65100;\">Risks and Concerns Identified</h2>\n      <pre style=\"font-size:13px;color:#444;line-height:1.8;white-space:pre-wrap;margin:0;\">{{ $json.risksStr }}</pre>\n    </div>\n\n    {{ $json.notes ? '<div style=\"background:#f3e5f5;border-left:4px solid #7b1fa2;padding:14px;border-radius:0 6px 6px 0;\"><h2 style=\"margin:0 0 6px;font-size:14px;color:#4a148c;\">Submitter Notes</h2><p style=\"margin:0;font-size:13px;color:#444;\">' + $json.notes + '</p></div>' : '' }}\n\n  </div>\n\n  <div style=\"background:#c62828;padding:12px 24px;text-align:center;border-radius:0 0 8px 8px;\">\n    <p style=\"color:rgba(255,255,255,0.7);font-size:11px;margin:0;\">Vendor Proposal Evaluator \u2014 Powered by n8n + ChatGPT AI &nbsp;|&nbsp; Decision: REVIEW NEEDED</p>\n  </div>\n\n</div>",
        "options": {
          "senderName": "Vendor Evaluation System"
        },
        "subject": "=Vendor Proposal Needs Review \u2014 {{ $json.vendorName }} ({{ $json.riskCount }} risks found)"
      },
      "typeVersion": 2.1
    },
    {
      "id": "89ea1216-ff6a-4e51-97ad-5e7e5421d94c",
      "name": "8b. Gmail \u2014 Auto-Approval Notification",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1088,
        384
      ],
      "parameters": {
        "sendTo": "YOUR_APPROVER_EMAIL",
        "message": "=<div style=\"font-family:Arial,sans-serif;max-width:680px;margin:0 auto;background:#f5f5f5;\">\n\n  <div style=\"background:#2e7d32;padding:24px 28px;border-radius:8px 8px 0 0;\">\n    <h1 style=\"color:#fff;font-size:20px;margin:0;\">Proposal Auto-Approved</h1>\n    <p style=\"color:rgba(255,255,255,0.85);font-size:13px;margin:6px 0 0;\">Submitted by {{ $json.submitter }} on {{ $json.submittedAt }}</p>\n  </div>\n\n  <div style=\"background:#1b5e20;padding:10px 24px;display:flex;gap:20px;flex-wrap:wrap;\">\n    <span style=\"color:#fff;font-size:13px;\">{{ $json.vendorName }}</span>\n    <span style=\"color:#fff;font-size:13px;\">{{ $json.category }}</span>\n    <span style=\"color:#c8e6c9;font-size:13px;font-weight:700;\">No risks detected</span>\n  </div>\n\n  <div style=\"background:#fff;padding:20px 24px;\">\n\n    <div style=\"background:#e8f5e9;border-left:4px solid #2e7d32;padding:14px;border-radius:0 6px 6px 0;margin-bottom:18px;\">\n      <h2 style=\"margin:0 0 8px;font-size:14px;color:#1a1a2e;\">Proposal Summary</h2>\n      <p style=\"margin:0;font-size:14px;color:#333;line-height:1.6;\">{{ $json.summary }}</p>\n    </div>\n\n    <h2 style=\"font-size:14px;color:#1a1a2e;border-bottom:2px solid #eee;padding-bottom:6px;\">Key Highlights</h2>\n    <pre style=\"font-size:13px;color:#333;line-height:1.8;white-space:pre-wrap;background:#f9f9f9;padding:12px;border-radius:6px;margin-bottom:16px;\">{{ $json.keyPointsStr }}</pre>\n\n    {{ $json.notes ? '<div style=\"background:#f3e5f5;border-left:4px solid #7b1fa2;padding:14px;border-radius:0 6px 6px 0;\"><h2 style=\"margin:0 0 6px;font-size:14px;color:#4a148c;\">Submitter Notes</h2><p style=\"margin:0;font-size:13px;color:#444;\">' + $json.notes + '</p></div>' : '' }}\n\n  </div>\n\n  <div style=\"background:#2e7d32;padding:12px 24px;text-align:center;border-radius:0 0 8px 8px;\">\n    <p style=\"color:rgba(255,255,255,0.7);font-size:11px;margin:0;\">Vendor Proposal Evaluator \u2014 Powered by n8n + ChatGPT AI &nbsp;|&nbsp; Decision: APPROVED</p>\n  </div>\n\n</div>",
        "options": {
          "senderName": "Vendor Evaluation System"
        },
        "subject": "=Vendor Proposal Auto-Approved \u2014 {{ $json.vendorName }}"
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "bea3e754-90d1-4dbd-93c0-033c9cda67fa",
  "nodeGroups": [],
  "connections": {
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "4. AI Agent \u2014 Evaluate Proposal",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "6. IF \u2014 Risks Found?": {
      "main": [
        [
          {
            "node": "7a. Sheets \u2014 Log Risky Proposal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "7b. Sheets \u2014 Log Approved Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. AI Agent \u2014 Evaluate Proposal": {
      "main": [
        [
          {
            "node": "5. Code \u2014 Parse AI Output and Risk Flag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7a. Sheets \u2014 Log Risky Proposal": {
      "main": [
        [
          {
            "node": "8a. Gmail \u2014 Risk Alert to Review Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7b. Sheets \u2014 Log Approved Proposal": {
      "main": [
        [
          {
            "node": "8b. Gmail \u2014 Auto-Approval Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Form \u2014 Upload Vendor Proposal PDF": {
      "main": [
        [
          {
            "node": "2. Extract From File \u2014 Parse PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Extract From File \u2014 Parse PDF Text": {
      "main": [
        [
          {
            "node": "3. Code \u2014 Clean PDF Text and Prep Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5. Code \u2014 Parse AI Output and Risk Flag": {
      "main": [
        [
          {
            "node": "6. IF \u2014 Risks Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. Code \u2014 Clean PDF Text and Prep Context": {
      "main": [
        [
          {
            "node": "4. AI Agent \u2014 Evaluate Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow collects vendor proposal PDFs via an n8n Form, extracts text from the PDF, uses an OpenAI ChatGPT model to summarize and flag risks, logs the evaluation to Google Sheets, and sends an approval or review-needed notification through Gmail. Receives a submission from…

Source: https://n8n.io/workflows/16596/ — 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 n8n workflow template automates your lead generation and follow-up process using AI. It captures leads through a form, enriches them with company data, classifies them into different categories,

Gmail, Output Parser Structured, Form Trigger +5
AI & RAG

The workflow runs every hour with a randomized delay of 5–20 minutes to help distribute load. It records the exact date and time a lead is emailed so you can track outreach. Follow-ups are automatical

Google Sheets, Agent, OpenAI Chat +5
AI & RAG

This n8n workflow automates turning short user ideas into production-ready real-estate marketing assets (photorealistic images and optional 360° videos). A form submission seeds a prompt board → an LL

Form Trigger, Google Sheets, Agent +6
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 collects your job preferences via an n8n form, uses Apify to scrape LinkedIn job listings, and has OpenAI screen and rank them. After you approve a shortlist by Gmail, it generates tailo

Form Trigger, HTTP Request, Agent +3