AutomationFlowsAI & RAG › Verify Loan Documents and Score Eligibility with Claude and Google Sheets

Verify Loan Documents and Score Eligibility with Claude and Google Sheets

ByOneclick AI Squad @oneclick-ai on n8n.io

This workflow receives loan applications via webhook, uses Anthropic Claude to verify document text and generate an underwriting recommendation, calculates an explainable risk score in code, logs each application to Google Sheets, and sends decision emails through SendGrid…

Webhook trigger★★★★☆ complexityAI-powered25 nodesAgentAnthropic ChatHTTP Request
AI & RAG Trigger: Webhook Nodes: 25 Complexity: ★★★★☆ AI nodes: yes Added:

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

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

The workflow JSON

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

Download .json
{
  "id": "K9z0tWtlwZJ2b8x4",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Verify Loan Documents and Score Eligibility with AI-Driven Risk Assessment",
  "tags": [],
  "nodes": [
    {
      "id": "6511fdc6-c81a-4b61-9784-6de2a8e62021",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        320
      ],
      "parameters": {
        "width": 1040,
        "height": 1480,
        "content": "## AI Loan Eligibility & Document Verification\n\nThis workflow verifies loan application documents, extracts and cross-checks applicant details, calculates a transparent, deterministic risk score from real financial ratios, and uses AI to generate an instant eligibility recommendation \u2014 with every application logged for audit and review.\n\nRisk scoring is deliberately NOT left entirely to the AI. Credit tier, debt-to-income ratio, employment tenure, bank account health, and document consistency are combined into a numeric risk score with a plain formula in code, so the number behind every decision is explainable and reproducible. AI is used where it adds real value: reading messy document text, spotting inconsistencies, and writing a clear, human-readable recommendation and rationale on top of that score.\n\n### Who's it for\n- Fintech lenders and credit unions wanting a first-pass automated underwriting layer\n- Loan officers who want obviously-good and obviously-bad applications resolved instantly, so they only spend time on the middle\n- Compliance and audit teams that need a reproducible, logged rationale behind every recommendation\n- Anyone replacing manual document review and spreadsheet-based scoring with automation\n\n### How it works\n1. Applicant submits personal details, loan request, and extracted text from their ID, income proof, and bank statement via webhook POST\n2. AI reads the three documents, extracts structured data, and cross-checks it against what the applicant declared, flagging missing or inconsistent information\n3. Applications with unusable or clearly incomplete documents get an immediate resubmission request instead of proceeding\n4. Code calculates debt-to-income ratio, loan-to-income ratio, an estimated monthly installment, and a weighted 0-100 risk score from credit tier, DTI, employment tenure, bank health, and document consistency\n5. AI reviews the numbers and documents and writes a recommendation (Approve, Conditional Approval, Manual Review, or Decline), a plain-language rationale, any conditions, and a suggested loan amount\n6. Every application is logged to Google Sheets regardless of outcome\n7. Approvable applications get an automated applicant email; everything else notifies the internal underwriting team and sends the applicant a status update\n8. Webhook responds with the full decision as JSON\n\n### How to set up\n1. Import this workflow into n8n\n2. Add Anthropic Claude credentials and attach to both Anthropic model nodes\n3. Add SendGrid credentials and replace YOUR_SENDGRID_API_KEY in all three email nodes\n4. Replace underwriting@yourlender.com with your real internal review team address\n5. Switch the Google Sheets HTTP node to the native Google Sheets node with OAuth2, or add OAuth2 credentials directly\n6. Replace YOUR_SHEET_ID with your actual Spreadsheet ID\n7. Set up your Google Sheet with a LoanApplications tab (column layout below)\n8. Activate and POST a test application to the webhook URL\n\n### Requirements\n- Anthropic Claude API key (claude-sonnet-4 recommended)\n- SendGrid API key for email delivery\n- Google Sheets with OAuth2 credentials (LoanApplications tab)\n- n8n instance with a publicly accessible webhook URL\n\n### How to customize\n- Adjust the interest rate tiers, ratio weights, and risk thresholds in JS - Calculate Financial Ratios & Risk Score to match your lending policy\n- Raise or lower the document verification threshold in IF - Documents Sufficient (default: score >= 4)\n- Replace the raw document-text fields with a real OCR step (e.g. fetch uploaded files, then call an OCR/Textract-style API) ahead of Stage 1\n- Add a second AI reviewer call for large loan amounts as a maker-checker control\n- Add an SMS notification alongside email for time-sensitive decisions\n\n### Important note\nThis workflow produces a recommendation to support human underwriters, not an automated final lending decision. Keep a qualified human in the loop before any adverse action, and confirm the scoring approach meets applicable fair-lending and credit regulations in your jurisdiction before production use.\n\n### Google Sheets Column Layout\n\nLoanApplications tab - row 1 headers:\nApplication ID | Applicant Name | Applicant Email | Loan Amount | Loan Purpose | Term Months | Verified Monthly Income | Debt-to-Income Ratio | Risk Score | Risk Tier | Recommendation | Suggested Loan Amount | Decided At"
      },
      "typeVersion": 1
    },
    {
      "id": "cbac695c-814f-45a5-aaf5-c05c97e3df26",
      "name": "Sticky Note - Stage 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        816
      ],
      "parameters": {
        "color": 4,
        "width": 1484,
        "height": 820,
        "content": "## Stage 1: Intake, Document Verification & Risk Scoring\n\nAccepts loan applications via webhook POST, including raw extracted text from the applicant's ID, income proof, and bank statement. All fields have sensible defaults so the workflow can be tested without a payload.\n\nThe verification AI (claude-sonnet-4, temperature 0.2 for accuracy) extracts structured data from each document and cross-checks it against the applicant's self-reported name, employer, and income, flagging any inconsistencies or missing documents.\n\nJS - Parse Document Verification Output extracts the AI JSON with a lenient fallback that trusts the applicant's self-reported figures if parsing fails.\n\nIF - Documents Sufficient sends an immediate resubmission request for unusable or clearly incomplete document sets, instead of proceeding with bad data.\n\nJS - Calculate Financial Ratios & Risk Score is pure code, not AI: it computes DTI, loan-to-income ratio, an estimated monthly installment from a credit-tier interest rate, and a weighted 0-100 risk score from credit tier, DTI, employment tenure, bank account health, and document consistency."
      },
      "typeVersion": 1
    },
    {
      "id": "55f5dc06-b3e6-41b6-b17a-fcc59eed75f8",
      "name": "Sticky Note - Stage 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2608,
        768
      ],
      "parameters": {
        "color": 4,
        "width": 716,
        "height": 820,
        "content": "## Stage 2: AI Eligibility Recommendation\n\nA Wait buffer respects API rate limits before this call.\n\nThe eligibility AI (claude-sonnet-4, temperature 0.3 for consistent, grounded judgment) reviews the applicant's request, the computed financial ratios, the risk score and tier, and any document consistency issues, then writes a recommendation: Approve, Conditional Approval, Manual Review, or Decline.\n\nIt also returns a plain-language rationale that references the actual numbers, any conditions required for a conditional approval, a suggested loan amount (which may differ from what was requested), and a confidence level.\n\nJS - Parse Eligibility Output extracts that JSON with a conservative fallback: if parsing fails, the application defaults to Manual Review rather than a silent approval or denial."
      },
      "typeVersion": 1
    },
    {
      "id": "995a235f-4000-471e-b51f-0f5122b56d06",
      "name": "Sticky Note - Stage 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        544
      ],
      "parameters": {
        "color": 4,
        "width": 1388,
        "height": 1066,
        "content": "## Stage 3: Logging & Decision Delivery\n\nJS - Format Final Application Record assembles the complete decision object and the summary row for the tracker sheet.\n\nSave Application to Google Sheets Tracker appends that row \u2014 every application is logged, regardless of outcome, for audit and reporting.\n\nIF - Auto-Decidable branches on the recommendation:\n- Approve or Conditional Approval: Send Applicant Approval Email delivers the decision, any conditions, and next steps directly to the applicant, then the webhook responds with the full decision.\n- Manual Review or Decline: Send Internal Review Notification Email gives the underwriting team the full context and AI rationale, Send Applicant Status Email lets the applicant know their application needs more time or was not approved, and the webhook responds with the same decision detail \u2014 all three fire in parallel."
      },
      "typeVersion": 1
    },
    {
      "id": "f728ae1f-9f75-40cd-8bea-282d3ddac27b",
      "name": "Webhook - Receive Loan Application",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1152,
        1248
      ],
      "parameters": {
        "path": "loan-application-inbound",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1.1
    },
    {
      "id": "2e6cca28-21b2-4dff-8b56-0dba13595770",
      "name": "Set - Prepare Application Context",
      "type": "n8n-nodes-base.set",
      "position": [
        1376,
        1248
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "applicantName",
              "type": "string",
              "value": "={{ $json.applicantName || $json.body?.applicantName || 'Jordan Casey' }}"
            },
            {
              "name": "applicantEmail",
              "type": "string",
              "value": "={{ $json.applicantEmail || $json.body?.applicantEmail || 'applicant@example.com' }}"
            },
            {
              "name": "applicantPhone",
              "type": "string",
              "value": "={{ $json.applicantPhone || $json.body?.applicantPhone || '' }}"
            },
            {
              "name": "dateOfBirth",
              "type": "string",
              "value": "={{ $json.dateOfBirth || $json.body?.dateOfBirth || '1990-01-01' }}"
            },
            {
              "name": "loanAmount",
              "type": "number",
              "value": "={{ $json.loanAmount || $json.body?.loanAmount || 15000 }}"
            },
            {
              "name": "loanPurpose",
              "type": "string",
              "value": "={{ $json.loanPurpose || $json.body?.loanPurpose || 'Debt consolidation' }}"
            },
            {
              "name": "monthlyIncome",
              "type": "number",
              "value": "={{ $json.monthlyIncome || $json.body?.monthlyIncome || 5200 }}"
            },
            {
              "name": "employmentStatus",
              "type": "string",
              "value": "={{ $json.employmentStatus || $json.body?.employmentStatus || 'Full-time employed' }}"
            },
            {
              "name": "employerName",
              "type": "string",
              "value": "={{ $json.employerName || $json.body?.employerName || 'Acme Corp' }}"
            },
            {
              "name": "yearsEmployed",
              "type": "number",
              "value": "={{ $json.yearsEmployed || $json.body?.yearsEmployed || 2 }}"
            },
            {
              "name": "selfReportedCreditScore",
              "type": "number",
              "value": "={{ $json.selfReportedCreditScore || $json.body?.selfReportedCreditScore || 680 }}"
            },
            {
              "name": "existingMonthlyDebt",
              "type": "number",
              "value": "={{ $json.existingMonthlyDebt || $json.body?.existingMonthlyDebt || 800 }}"
            },
            {
              "name": "requestedTermMonths",
              "type": "number",
              "value": "={{ $json.requestedTermMonths || $json.body?.requestedTermMonths || 36 }}"
            },
            {
              "name": "idDocumentText",
              "type": "string",
              "value": "={{ $json.idDocumentText || $json.body?.idDocumentText || 'Driver license: Jordan Casey, DOB 1990-01-01, ID number D1234567' }}"
            },
            {
              "name": "incomeProofText",
              "type": "string",
              "value": "={{ $json.incomeProofText || $json.body?.incomeProofText || 'Pay stub: Jordan Casey, Employer: Acme Corp, Gross monthly pay: 5200 USD' }}"
            },
            {
              "name": "bankStatementText",
              "type": "string",
              "value": "={{ $json.bankStatementText || $json.body?.bankStatementText || 'Checking account statement: average balance 3400 USD, two monthly deposits of 2600 USD each, no overdrafts' }}"
            },
            {
              "name": "submittedAt",
              "type": "string",
              "value": "={{ new Date().toISOString() }}"
            },
            {
              "name": "applicationId",
              "type": "string",
              "value": "={{ 'LOAN-' + Date.now().toString().slice(-8) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "275ad593-2311-4c29-9420-a4bae9b93d6a",
      "name": "AI - Verify & Extract Document Data",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1600,
        1248
      ],
      "parameters": {
        "text": "=You are a meticulous loan document verification specialist.\n\nApplicant declared:\nName: {{ $json.applicantName }}\nDate of Birth: {{ $json.dateOfBirth }}\nEmployer: {{ $json.employerName }}\nSelf-Reported Monthly Income: {{ $json.monthlyIncome }}\n\nDocuments provided (raw extracted text):\nID Document: {{ $json.idDocumentText }}\nIncome Proof: {{ $json.incomeProofText }}\nBank Statement: {{ $json.bankStatementText }}\n\nYour task:\n1. Extract structured data from each document.\n2. Cross-check the extracted name, employer, and income against what the applicant declared.\n3. Flag any document that is missing, illegible, incomplete, or looks inconsistent or fabricated.\n\nReturn a JSON object with EXACTLY these fields:\n{\n  \"documentsComplete\": true or false,\n  \"extractedName\": \"Name found on the ID document\",\n  \"extractedDOB\": \"Date of birth found on the ID document\",\n  \"extractedIdNumber\": \"ID number found on the ID document\",\n  \"extractedEmployer\": \"Employer found on the income proof\",\n  \"extractedMonthlyIncome\": <number, monthly income found on the income proof>,\n  \"bankAverageBalance\": <number, average balance found on the bank statement>,\n  \"bankAverageMonthlyDeposit\": <number, average monthly deposit found on the bank statement>,\n  \"overdraftCount\": <integer, number of overdrafts mentioned>,\n  \"consistencyIssues\": [\"issue1\", \"issue2\"],\n  \"verificationNotes\": \"Brief summary of the verification outcome\",\n  \"verificationScore\": <number 1-10, 10 = fully complete, consistent, verifiable documents>\n}\n\nIf no consistency issues are found, return an empty array. Return ONLY the JSON. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "caafa542-c3b4-4270-b94d-6e62c0677706",
      "name": "Anthropic - Verification Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1680,
        1472
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.2
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "0ee382d0-332a-4265-b9b7-a998d41072b9",
      "name": "JS - Parse Document Verification Output",
      "type": "n8n-nodes-base.code",
      "position": [
        1952,
        1248
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch(e) {\n  parsed = {\n    documentsComplete: true,\n    extractedName: item.applicantName,\n    extractedDOB: item.dateOfBirth,\n    extractedIdNumber: 'Unavailable',\n    extractedEmployer: item.employerName,\n    extractedMonthlyIncome: item.monthlyIncome,\n    bankAverageBalance: 0,\n    bankAverageMonthlyDeposit: item.monthlyIncome,\n    overdraftCount: 0,\n    consistencyIssues: [],\n    verificationNotes: 'Auto-passed: could not parse AI verification output, trusting self-reported figures',\n    verificationScore: 5\n  };\n}\nreturn { json: { ...item, ...parsed } };"
      },
      "typeVersion": 2
    },
    {
      "id": "2f7fd411-947a-4c0a-95af-c4ca94cf48d2",
      "name": "IF - Documents Sufficient",
      "type": "n8n-nodes-base.if",
      "position": [
        2176,
        1248
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-verification-score",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.verificationScore }}",
              "rightValue": 4
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a3a5ab05-5ab0-484a-b859-31599c003487",
      "name": "Respond to Webhook - Documents Insufficient",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2400,
        1344
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"success\": false,\n  \"applicationId\": \"{{ $json.applicationId }}\",\n  \"status\": \"Documents Insufficient\",\n  \"verificationNotes\": \"{{ $json.verificationNotes }}\",\n  \"consistencyIssues\": {{ JSON.stringify($json.consistencyIssues) }},\n  \"message\": \"We could not verify your documents. Please resubmit clearer or complete copies of your ID, income proof, and bank statement.\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "807178d1-2680-4247-abe3-4a59f22aa4c0",
      "name": "JS - Calculate Financial Ratios & Risk Score",
      "type": "n8n-nodes-base.code",
      "position": [
        2400,
        1152
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst verifiedIncome = (item.extractedMonthlyIncome && item.extractedMonthlyIncome > 0) ? item.extractedMonthlyIncome : item.monthlyIncome;\nconst existingDebt = item.existingMonthlyDebt || 0;\n\n// Estimate an illustrative interest rate tier from self-reported credit score\nconst creditScore = item.selfReportedCreditScore || 650;\nlet annualRatePercent;\nif (creditScore >= 740) annualRatePercent = 7.5;\nelse if (creditScore >= 670) annualRatePercent = 10.5;\nelse if (creditScore >= 580) annualRatePercent = 15.0;\nelse annualRatePercent = 22.0;\n\nconst monthlyRate = annualRatePercent / 100 / 12;\nconst n = item.requestedTermMonths || 36;\nconst principal = item.loanAmount || 0;\nconst estimatedMonthlyInstallment = monthlyRate > 0\n  ? Math.round((principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -n)) * 100) / 100\n  : Math.round((principal / n) * 100) / 100;\n\nconst debtToIncomeRatio = verifiedIncome > 0 ? Math.round(((existingDebt + estimatedMonthlyInstallment) / verifiedIncome) * 100) : 100;\nconst loanToAnnualIncomeRatio = verifiedIncome > 0 ? Math.round((principal / (verifiedIncome * 12)) * 100) : 100;\nconst disposableIncomeAfterLoan = Math.round((verifiedIncome - existingDebt - estimatedMonthlyInstallment) * 100) / 100;\n\n// Component scores, 0-100, higher is better (lower risk)\nconst creditComponent = Math.max(0, Math.min(100, ((creditScore - 300) / (850 - 300)) * 100));\nconst dtiComponent = Math.max(0, 100 - debtToIncomeRatio);\nconst tenureComponent = Math.min(100, (item.yearsEmployed || 0) * 20);\nconst bankHealthComponent = Math.max(0, 100 - (item.overdraftCount || 0) * 25);\nconst consistencyComponent = Math.max(0, 100 - ((item.consistencyIssues ? item.consistencyIssues.length : 0) * 20));\n\nconst compositeScore = Math.round(\n  creditComponent * 0.35 +\n  dtiComponent * 0.30 +\n  tenureComponent * 0.15 +\n  bankHealthComponent * 0.10 +\n  consistencyComponent * 0.10\n);\n\nconst riskScore = 100 - compositeScore;\nlet riskTier = 'Low Risk';\nif (riskScore >= 70) riskTier = 'Very High Risk';\nelse if (riskScore >= 50) riskTier = 'High Risk';\nelse if (riskScore >= 30) riskTier = 'Medium Risk';\n\nreturn {\n  json: {\n    ...item,\n    verifiedMonthlyIncome: verifiedIncome,\n    annualRatePercent,\n    estimatedMonthlyInstallment,\n    debtToIncomeRatio,\n    loanToAnnualIncomeRatio,\n    disposableIncomeAfterLoan,\n    riskScore,\n    riskTier\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b0f2b26b-3d5e-4f2a-af0a-c2411ebf2bbd",
      "name": "Wait - Before Eligibility AI",
      "type": "n8n-nodes-base.wait",
      "position": [
        2624,
        1152
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "7383ee30-0bf0-44dc-a809-c32edcb4c24e",
      "name": "AI - Generate Eligibility Assessment & Recommendation",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2848,
        1152
      ],
      "parameters": {
        "text": "=You are an underwriting assistant producing a recommendation for a human loan officer, not a final automated decision.\n\nApplicant: {{ $json.applicantName }}\nLoan Amount Requested: {{ $json.loanAmount }}\nLoan Purpose: {{ $json.loanPurpose }}\nRequested Term: {{ $json.requestedTermMonths }} months\nEmployment: {{ $json.employmentStatus }}, {{ $json.yearsEmployed }} years at current employer\nSelf-Reported Credit Score: {{ $json.selfReportedCreditScore }}\nVerified Monthly Income: {{ $json.verifiedMonthlyIncome }}\nEstimated Monthly Installment: {{ $json.estimatedMonthlyInstallment }} at {{ $json.annualRatePercent }}% illustrative annual rate\nDebt-to-Income Ratio: {{ $json.debtToIncomeRatio }}%\nLoan-to-Annual-Income Ratio: {{ $json.loanToAnnualIncomeRatio }}%\nDisposable Income After Loan: {{ $json.disposableIncomeAfterLoan }}\nRisk Score: {{ $json.riskScore }}/100 ({{ $json.riskTier }})\nDocument Consistency Issues: {{ JSON.stringify($json.consistencyIssues) }}\nVerification Notes: {{ $json.verificationNotes }}\n\nGuidance: Very High Risk or a debt-to-income ratio above 50% should generally lean toward Manual Review or Decline. Medium or High Risk with minor issues can lean toward Conditional Approval with specific conditions. Low Risk with no consistency issues can lean toward Approve. Always ground your reasoning in the actual numbers above.\n\nReturn a JSON object with EXACTLY these fields:\n{\n  \"recommendation\": \"Approve|Conditional Approval|Manual Review|Decline\",\n  \"reasoning\": \"Two to four sentences referencing the specific numbers above\",\n  \"conditions\": [\"condition1\", \"condition2\"],\n  \"suggestedLoanAmount\": <number, may equal the requested amount or be a lower counteroffer>,\n  \"recommendationConfidence\": <number 0-100>\n}\n\nIf no conditions apply, return an empty array. Return ONLY the JSON. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "eaa0bf69-4b69-4182-a2ba-f4e3246def9a",
      "name": "Anthropic - Eligibility Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        2928,
        1376
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.3
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f38a7566-1fca-4e78-ae2f-3481edd5f832",
      "name": "JS - Parse Eligibility Output",
      "type": "n8n-nodes-base.code",
      "position": [
        3200,
        1152
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch(e) {\n  parsed = {\n    recommendation: 'Manual Review',\n    reasoning: 'Auto-flagged: could not parse AI eligibility output, routing to a human underwriter as a precaution.',\n    conditions: [],\n    suggestedLoanAmount: item.loanAmount,\n    recommendationConfidence: 50\n  };\n}\nreturn { json: { ...item, ...parsed } };"
      },
      "typeVersion": 2
    },
    {
      "id": "ab098565-e798-4045-a3ba-e496f8ce4644",
      "name": "JS - Format Final Application Record",
      "type": "n8n-nodes-base.code",
      "position": [
        3632,
        1152
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst decidedAt = new Date().toISOString();\n\nreturn {\n  json: {\n    ...item,\n    decidedAt,\n    applicationSummaryRow: [\n      item.applicationId || 'N/A',\n      item.applicantName || '',\n      item.applicantEmail || '',\n      item.loanAmount || 0,\n      item.loanPurpose || '',\n      item.requestedTermMonths || 0,\n      item.verifiedMonthlyIncome || 0,\n      (item.debtToIncomeRatio || 0) + '%',\n      item.riskScore || 0,\n      item.riskTier || '',\n      item.recommendation || '',\n      item.suggestedLoanAmount || 0,\n      decidedAt\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "6957de85-4c19-45a1-9730-43ca89008706",
      "name": "Save Application to Google Sheets Tracker",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3856,
        1152
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/LoanApplications!A1:append?valueInputOption=USER_ENTERED",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"values\": [{{ JSON.stringify($json.applicationSummaryRow) }}]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ab147b6c-ba90-480c-ba7b-60ccb55beb7c",
      "name": "IF - Auto-Decidable",
      "type": "n8n-nodes-base.if",
      "position": [
        4080,
        1152
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "cond-approve",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.recommendation }}",
              "rightValue": "Approve"
            },
            {
              "id": "cond-conditional",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.recommendation }}",
              "rightValue": "Conditional Approval"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1c357d4d-71fd-47b4-ae0a-cd9a9435575d",
      "name": "Send Applicant Approval Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4304,
        864
      ],
      "parameters": {
        "url": "https://api.sendgrid.com/v3/mail/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"personalizations\": [{\"to\": [{\"email\": \"{{ $json.applicantEmail }}\"}]}],\n  \"from\": {\"email\": \"loans@yourlender.com\", \"name\": \"Loan Applications Team\"},\n  \"subject\": \"Your loan application {{ $json.applicationId }}: {{ $json.recommendation }}\",\n  \"content\": [{\n    \"type\": \"text/plain\",\n    \"value\": \"Hi {{ $json.applicantName }},\\n\\nYour application {{ $json.applicationId }} for {{ $json.loanAmount }} has a recommendation of {{ $json.recommendation }}.\\n\\n{{ $json.reasoning }}\\n\\nSuggested loan amount: {{ $json.suggestedLoanAmount }}\\nEstimated monthly installment: {{ $json.estimatedMonthlyInstallment }}\\nConditions: {{ JSON.stringify($json.conditions) }}\\n\\nA member of our team will follow up with final paperwork and next steps.\"\n  }]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3319eeda-81ac-4cbc-b8a5-145966aeba20",
      "name": "Respond to Webhook - Approved",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4528,
        864
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"applicationId\": \"{{ $json.applicationId }}\",\n  \"recommendation\": \"{{ $json.recommendation }}\",\n  \"riskScore\": {{ $json.riskScore }},\n  \"riskTier\": \"{{ $json.riskTier }}\",\n  \"suggestedLoanAmount\": {{ $json.suggestedLoanAmount }},\n  \"estimatedMonthlyInstallment\": {{ $json.estimatedMonthlyInstallment }},\n  \"reasoning\": \"{{ $json.reasoning }}\",\n  \"conditions\": {{ JSON.stringify($json.conditions) }},\n  \"message\": \"Application processed. Confirmation email sent to the applicant.\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "790f6c45-cd13-4db1-9209-bc7a19503561",
      "name": "Send Internal Review Notification Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4304,
        1056
      ],
      "parameters": {
        "url": "https://api.sendgrid.com/v3/mail/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"personalizations\": [{\"to\": [{\"email\": \"underwriting@yourlender.com\"}]}],\n  \"from\": {\"email\": \"loans@yourlender.com\", \"name\": \"Loan Applications System\"},\n  \"subject\": \"{{ $json.recommendation }} required: application {{ $json.applicationId }}\",\n  \"content\": [{\n    \"type\": \"text/plain\",\n    \"value\": \"Application {{ $json.applicationId }} for {{ $json.applicantName }} ({{ $json.applicantEmail }}) needs attention.\\n\\nRecommendation: {{ $json.recommendation }}\\nReasoning: {{ $json.reasoning }}\\n\\nLoan Amount: {{ $json.loanAmount }} | Suggested: {{ $json.suggestedLoanAmount }}\\nRisk Score: {{ $json.riskScore }} ({{ $json.riskTier }})\\nDebt-to-Income Ratio: {{ $json.debtToIncomeRatio }}%\\nVerified Monthly Income: {{ $json.verifiedMonthlyIncome }}\\nDocument Consistency Issues: {{ JSON.stringify($json.consistencyIssues) }}\\nVerification Notes: {{ $json.verificationNotes }}\"\n  }]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "497ca76a-3f4e-46ee-bdc1-389fa3bc6ac7",
      "name": "Send Applicant Status Email",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4304,
        1248
      ],
      "parameters": {
        "url": "https://api.sendgrid.com/v3/mail/send",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"personalizations\": [{\"to\": [{\"email\": \"{{ $json.applicantEmail }}\"}]}],\n  \"from\": {\"email\": \"loans@yourlender.com\", \"name\": \"Loan Applications Team\"},\n  \"subject\": \"An update on your loan application {{ $json.applicationId }}\",\n  \"content\": [{\n    \"type\": \"text/plain\",\n    \"value\": \"Hi {{ $json.applicantName }},\\n\\nThank you for your application {{ $json.applicationId }}. {{ $json.recommendation === 'Decline' ? 'After review, we are unable to approve this application at this time.' : 'Your application needs a closer look from our underwriting team before we can confirm a decision.' }}\\n\\nWe will follow up with more information soon. If you have questions in the meantime, please reply to this email.\"\n  }]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "40c712df-9e05-4dd7-8421-4422f92686d3",
      "name": "Respond to Webhook - Under Review or Declined",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        4304,
        1440
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n  \"success\": true,\n  \"applicationId\": \"{{ $json.applicationId }}\",\n  \"recommendation\": \"{{ $json.recommendation }}\",\n  \"riskScore\": {{ $json.riskScore }},\n  \"riskTier\": \"{{ $json.riskTier }}\",\n  \"suggestedLoanAmount\": {{ $json.suggestedLoanAmount }},\n  \"reasoning\": \"{{ $json.reasoning }}\",\n  \"conditions\": {{ JSON.stringify($json.conditions) }},\n  \"message\": \"Application logged. The underwriting team has been notified and the applicant has been sent a status update.\"\n}"
      },
      "typeVersion": 1
    },
    {
      "id": "ae271036-6cee-46b7-a7ff-53efb15ea030",
      "name": "Wait For Result",
      "type": "n8n-nodes-base.wait",
      "position": [
        3408,
        1152
      ],
      "parameters": {},
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "06a80f17-955b-4711-aa62-bd61c360f8c8",
  "connections": {
    "Wait For Result": {
      "main": [
        [
          {
            "node": "JS - Format Final Application Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Auto-Decidable": {
      "main": [
        [
          {
            "node": "Send Applicant Approval Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Internal Review Notification Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Applicant Status Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond to Webhook - Under Review or Declined",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Documents Sufficient": {
      "main": [
        [
          {
            "node": "JS - Calculate Financial Ratios & Risk Score",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond to Webhook - Documents Insufficient",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - Before Eligibility AI": {
      "main": [
        [
          {
            "node": "AI - Generate Eligibility Assessment & Recommendation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic - Eligibility Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Eligibility Assessment & Recommendation",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse Eligibility Output": {
      "main": [
        [
          {
            "node": "Wait For Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Applicant Approval Email": {
      "main": [
        [
          {
            "node": "Respond to Webhook - Approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic - Verification Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Verify & Extract Document Data",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Set - Prepare Application Context": {
      "main": [
        [
          {
            "node": "AI - Verify & Extract Document Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Loan Application": {
      "main": [
        [
          {
            "node": "Set - Prepare Application Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Verify & Extract Document Data": {
      "main": [
        [
          {
            "node": "JS - Parse Document Verification Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Format Final Application Record": {
      "main": [
        [
          {
            "node": "Save Application to Google Sheets Tracker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse Document Verification Output": {
      "main": [
        [
          {
            "node": "IF - Documents Sufficient",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Internal Review Notification Email": {
      "main": [
        []
      ]
    },
    "Save Application to Google Sheets Tracker": {
      "main": [
        [
          {
            "node": "IF - Auto-Decidable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Calculate Financial Ratios & Risk Score": {
      "main": [
        [
          {
            "node": "Wait - Before Eligibility AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Eligibility Assessment & Recommendation": {
      "main": [
        [
          {
            "node": "JS - Parse Eligibility Output",
            "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 receives loan applications via webhook, uses Anthropic Claude to verify document text and generate an underwriting recommendation, calculates an explainable risk score in code, logs each application to Google Sheets, and sends decision emails through SendGrid…

Source: https://n8n.io/workflows/17105/ — 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

⏺ 🚀 How it works

Agent, Anthropic Chat, Output Parser Structured +6
AI & RAG

WhatsApp AI Agent - Template. Uses redis, httpRequest, postgres, agent. Webhook trigger; 89 nodes.

Redis, HTTP Request, Postgres +2
AI & RAG

Airtable Operations System: Leads, AI Agent, Outreach, Sync, Canva, Make/Zapier Bridge. Uses airtable, anthropic, slack, agent. Webhook trigger; 82 nodes.

Airtable, Anthropic, Slack +12
AI & RAG

Lead Pipeline v3.0. Uses httpRequest, agent, lmChatAnthropic, toolThink. Webhook trigger; 77 nodes.

HTTP Request, Agent, Anthropic Chat +4
AI & RAG

What if AI didn't just write content—but actually thought about how to write it? This n8n workflow revolutionizes content creation by deploying multiple specialized AI agents that handle every aspect

Tool Http Request, Anthropic Chat, Airtable +7