AI-Powered Loan Pre-Screening Agent — built on n8n
A complete technical walkthrough: how SecureBank India automated loan intake decisions in under 10 seconds using n8n, OpenAI GPT-4o-mini, Neon Postgres, and Resend.
Automating loan intake from form to decision
SecureBank India implemented an intelligent loan pre-screening agent that automates the initial evaluation of loan applications. The system integrates a web-based application form, a simulated credit bureau lookup, an AI decision engine powered by OpenAI GPT-4o-mini, automated email notifications via Resend, and a persistent audit database on Neon Postgres — all orchestrated through the n8n workflow automation platform.
Instant decisions
Pre-screening decisions delivered in under 10 seconds per application, 24/7, with no human in the loop.
Consistent AI evaluation
Rule-based GPT-4o-mini scoring eliminates inconsistency and human bias at the intake stage.
Automated notifications
Applicants and loan officers receive tailored emails the moment a decision is made.
Full audit trail
Every decision, risk score, and AI flag logged to Neon Postgres for regulatory compliance.
Three-tier routing
Approve, Manual Review, or Reject — each with its own email flow and officer alert logic.
Zero-code form
Applicant-facing form hosted inside n8n — no separate frontend or external tool needed.
Four tools. One pipeline.
All components are cloud-native and free-tier friendly — ideal for a working demo that can go to production without a rewrite.
| Component | Technology | Notes |
|---|---|---|
| Workflow Engine | n8n | Self-hosted or n8n Cloud |
| Application Form | n8n Form Trigger | Built-in hosted form, no frontend needed |
| AI Decision Engine | OpenAI GPT-4o-mini | Via n8n AI Agent node |
| Email Service | Resend API | Transactional email |
| Database | Neon Postgres | Serverless, cloud, free tier available |
| Credit Bureau | Simulated CIBIL | Code node — replace with real API in production |
| Deployment | n8n Cloud or Docker | Self-hosted option available |
11 nodes, one linear flow, three possible outcomes
The workflow is linear until Node 6 (the Switch). Data passes from one node to the next, accumulating context — form fields, credit score, DTI ratio, and finally the AI decision — before branching into three email paths.
(loan × 2%) ÷ income × 100.decision field and routes: approve → applicant email, manual_review → officer alert, reject → rejection email.loan_applications audit table.Data flow
Each node passes its complete output to the next node. By the time data reaches the Switch node, the JSON object contains all original form fields plus enrichment data (credit score, DTI ratio) and AI output (decision, risk score, flags, recommended amount, interest band).
Three outcomes, driven by three signals
The AI evaluates every application against credit score, debt-to-income ratio, and employment stability. Each combination maps to one of three outcomes.
All criteria met
- Credit score 720 or above
- DTI ratio below 35%
- Salaried or business owner
- Age between 21 and 65
Approval email sent to applicant instantly
Borderline signals
- Credit score 600–719
- DTI ratio 35%–50%
- Self-employed applicant
- Any one elevated flag
Loan officer alerted with full risk breakdown
Clear disqualifiers
- Credit score below 600
- DTI ratio above 50%
- Currently unemployed
- Age outside 21–65 range
Polite rejection email sent with reapplication guidance
Setting up before you build
Neon Postgres — database setup
- Go to console.neon.tech and create a new project
- Project name:
loan-prescreening-demo— Region: AWS ap-south-1 (Mumbai) - Copy the connection string — needed for the n8n credential
- Open the SQL Editor and run the table creation script below
Database table SQL
CREATE TABLE loan_applications (
id SERIAL PRIMARY KEY,
applicant_name TEXT NOT NULL,
email TEXT NOT NULL,
phone TEXT,
age INT,
employment TEXT,
monthly_income NUMERIC(12,2),
loan_amount NUMERIC(12,2),
loan_purpose TEXT,
credit_score INT,
dti_ratio NUMERIC(5,2),
decision TEXT,
risk_score INT,
ai_summary TEXT,
flags TEXT,
submitted_at TIMESTAMPTZ DEFAULT NOW(),
processed_at TIMESTAMPTZ
);
n8n Neon credential settings
| Credential name | Neon-LoanDB |
| Host | ep-xxxx.ap-southeast-1.aws.neon.tech |
| Database | neondb |
| User | neondb_owner |
| Password | <your neon password> |
| Port | 5432 |
| SSL | Enabled (required by Neon) |
Resend Email — credential setup
- Go to resend.com and create an account
- Navigate to API Keys → Create API Key — Name:
n8n-loan-agent, Permission: Full Access - Copy the key (starts with
re_...) - For demo: use
onboarding@resend.devas sender — works without domain verification
| Credential type | Header Auth |
| Credential name | Resend-API |
| Header name | Authorization |
| Header value | Bearer re_YOUR_API_KEY_HERE |
OpenAI — API key setup
- Go to platform.openai.com and create an API key
- In n8n: Settings → Credentials → Add Credential → OpenAI API
- Credential name:
OpenAI-LoanAgent— paste key, Save, then Test credential
What each node actually does
Click any node below to expand configuration details and key implementation notes.
1 Loan Application Form n8n Form Trigger · URL path: /loan-apply ▼
| Node type | n8n Form Trigger |
| Node name | Loan Application Form |
| URL path | /loan-apply |
| Form title | Apply for a Loan — SecureBank |
| Button label | Submit Application |
Form fields: Full Name (Text, Required), Email Address (Email, Required), Phone Number (Text, Optional), Date of Birth (Date, Required), Employment Status (Dropdown: Salaried / Self-Employed / Business Owner / Unemployed, Required), Monthly Income (Number, Required), Loan Amount (Number, Required), Loan Purpose (Dropdown: Home / Car / Personal / Education / Business Loan, Required), PAN Number (Text, Required).
2 Validate & Prepare Data Code node · JavaScript ▼
Trims all field keys to handle leading/trailing spaces, validates required fields, calculates age from DOB (throws error if outside 21–65), and computes DTI ratio using estimatedEMI = loanAmount × 0.02 then DTI = (EMI ÷ monthlyIncome) × 100.
const d = $input.first().json;
const data = {};
Object.keys(d).forEach(key => { data[key.trim()] = d[key]; });
const name = data['Full Name']?.toString().trim();
const email = data['Email Address']?.toString().trim();
const income = parseFloat(data['Monthly Income'] || 0);
const loan = parseFloat(data['Loan Amount'] || 0);
const dob = data['Date of Birth'];
const emp = data['Employment Status'];
const purpose= data['Loan Purpose'];
const pan = data['PAN Number']?.toString().trim();
const missing = [];
if (!name) missing.push('Full Name');
if (!email) missing.push('Email');
if (!income) missing.push('Monthly Income');
if (!loan) missing.push('Loan Amount');
if (!dob) missing.push('Date of Birth');
if (!pan) missing.push('PAN Number');
if (missing.length > 0) throw new Error('Missing: ' + missing.join(', '));
const age = Math.floor((new Date() - new Date(dob)) / (365.25*24*3600*1000));
if (age < 21 || age > 65) throw new Error('Age must be 21-65. Got: ' + age);
const estimatedEMI = loan * 0.02;
const dtiRatio = parseFloat(((estimatedEMI / income) * 100).toFixed(2));
return [{ json: {
applicant_name: name, email, phone: data['Phone Number'] || '',
age, employment: emp, monthly_income: income, loan_amount: loan,
loan_purpose: purpose, pan_number: pan, dti_ratio: dtiRatio,
submitted_at: data['submittedAt'] || new Date().toISOString()
}}];
3 Mock Credit Bureau Code node · Replace with real CIBIL API in production ▼
Simulates a CIBIL score for demo purposes. Starts at base 650, adjusts for income level (±60), DTI ratio (±80), and employment status (+20 salaried, −150 unemployed). Adds small random variation for realism. In production, replace this node with an HTTP Request to CIBIL/Experian using the applicant’s PAN number.
const d = $input.first().json;
let baseScore = 650;
if (d.monthly_income > 100000) baseScore += 60;
else if (d.monthly_income > 60000) baseScore += 30;
else if (d.monthly_income < 30000) baseScore -= 50;
if (d.dti_ratio < 20) baseScore += 40;
else if (d.dti_ratio > 50) baseScore -= 80;
else if (d.dti_ratio > 35) baseScore -= 30;
if (d.employment === 'Salaried') baseScore += 20;
if (d.employment === 'Unemployed') baseScore -= 150;
baseScore += Math.floor(Math.random() * 30) - 10;
const creditScore = Math.min(900, Math.max(300, baseScore));
return [{ json: { ...d, credit_score: creditScore,
bureau_name: 'CIBIL (Simulated)', bureau_status: 'Active',
checked_at: new Date().toISOString() }}];
4 Loan Screening AI (GPT-4o-mini) AI Agent node · Temperature: 0.1 · Max tokens: 600 ▼
| Sub-node type | OpenAI Chat Model |
| Credential | OpenAI-LoanAgent |
| Model | gpt-4o-mini |
| Temperature | 0.1 |
| Max Tokens | 600 |
System message
You are a senior loan pre-screening AI for SecureBank India.
Evaluate the applicant data and respond ONLY with a valid JSON object.
No markdown, no explanation outside the JSON.
JSON schema you must follow exactly:
{
"decision": "approve" | "manual_review" | "reject",
"risk_score": integer between 0 and 100 (higher = riskier),
"summary": "2 sentence plain English explanation",
"flags": ["concern 1", "concern 2"],
"recommended_amount": number or null,
"interest_band": "e.g. 8.5%-10.2%" or null
}
Decision rules:
- approve : credit_score >= 720 AND DTI < 35% AND stable employment
- manual_review : credit_score 600-719 OR DTI 35-50% OR self-employed
- reject : credit_score < 600 OR DTI > 50% OR unemployed
- Always reject if age < 21 or age > 65
- recommended_amount must not exceed 60x monthly income
User message / prompt
Applicant: {{ $json.applicant_name }}
Age: {{ $json.age }}
Employment: {{ $json.employment }}
Monthly Income: Rs {{ $json.monthly_income }}
Loan Requested: Rs {{ $json.loan_amount }}
Loan Purpose: {{ $json.loan_purpose }}
Credit Score: {{ $json.credit_score }}
DTI Ratio: {{ $json.dti_ratio }}%
PAN: {{ $json.pan_number }}
5 Parse AI Response Code node · JavaScript ▼
Reads the AI output field (tries output, text, response, message.content for n8n version compatibility), strips markdown fences, parses JSON, and merges structured fields back into the item.
const input = $input.first().json;
const raw = input.output || input.text || input.response
|| input.message?.content || input.choices?.[0]?.message?.content || '';
const cleaned = raw.replace(/```json/gi,'').replace(/```/g,'').trim();
let ai;
try { ai = JSON.parse(cleaned); }
catch(e) { throw new Error('AI did not return valid JSON. Raw: ' + raw); }
return [{ json: { ...input,
decision: ai.decision, risk_score: ai.risk_score,
ai_summary: ai.summary,
flags: Array.isArray(ai.flags) ? ai.flags.join(', ') : '',
recommended_amount: ai.recommended_amount || null,
interest_band: ai.interest_band || null
}}];
6
Route Decision (Switch node)
Evaluates {{ $json.decision }}
▼
| Field to evaluate | {{ $json.decision }} |
| Route 1 | equals approve → Send Approval Email |
| Route 2 | equals manual_review → Alert Loan Officer |
| Route 3 | equals reject → Send Rejection Email |
7–9 Email Notifications — 3 branches HTTP Request · POST to Resend API ▼
All three nodes POST to https://api.resend.com/emails with Header Auth → Resend-API. Each sends a different HTML email body with n8n expressions injecting applicant data.
| Node 7 — Approval | Sent to {{ $json.email }} — includes loan amount, recommended amount, interest band, risk score |
| Node 8 — Officer alert | Sent to loan.officer@yourdomain.com — includes full applicant data, credit score, DTI, flags |
| Node 9 — Rejection | Sent to {{ $json.email }} — polite decline, 90-day reapplication guidance |
10–11 Merge + Save to Neon Postgres Merge (Append mode) then Postgres Execute Query ▼
The Merge node (Append mode) reunites all three branches. The Postgres node then inserts the complete record. The query uses RETURNING id so the application ID is available for downstream use.
INSERT INTO loan_applications (
applicant_name, email, phone, age, employment,
monthly_income, loan_amount, loan_purpose,
credit_score, dti_ratio, decision, risk_score,
ai_summary, flags, processed_at
) VALUES (
'{{ $json.applicant_name }}', '{{ $json.email }}',
'{{ $json.phone }}', {{ $json.age }},
'{{ $json.employment }}', {{ $json.monthly_income }},
{{ $json.loan_amount }}, '{{ $json.loan_purpose }}',
{{ $json.credit_score }}, {{ $json.dti_ratio }},
'{{ $json.decision }}', {{ $json.risk_score }},
'{{ $json.ai_summary }}', '{{ $json.flags }}', NOW()
) RETURNING id;
Three test cases, every decision path covered
Submit these in sequence during a client demo. Each email arrives within 10 seconds of form submission.
Strong income, stable employment, low DTI. Risk score 15–30 range.
Self-employed flag + borderline DTI. Loan officer receives detailed alert.
Unemployed, very high DTI, low credit score. Automatic rejection.
Verify data in Neon after the demo
SELECT id, applicant_name, decision, risk_score,
credit_score, dti_ratio, flags, processed_at
FROM loan_applications
ORDER BY processed_at DESC
LIMIT 10;
Common issues and how to fix them
“Missing required fields” error in Node 2 ▼
“No prompt specified” error in the AI Agent ▼
AI returned invalid JSON error ▼
Emails are not being delivered ▼
onboarding@resend.dev — works without domain verification. For production, verify your domain in the Resend dashboard. Confirm the Authorization header starts with Bearer re_.Neon Postgres connection refused ▼
Switch node not routing correctly ▼
decision field contains exactly approve, manual_review, or reject — no uppercase, no extra whitespace. Check Node 5’s output panel.Credit score always the same ▼
Math.random(). For a fully consistent demo score, remove the Math.random() line from Node 3.Five changes before going live
The demo is intentionally minimal. Here is what to add before putting real applicants through the system.
Real credit bureau API
Replace Node 3 with an HTTP Request to CIBIL or Experian. Pass the PAN number as the lookup key and map the response into the same credit_score field.
KYC & identity verification
Add a parallel branch after Node 2 calling Onfido or DigiLocker. Merge the KYC result with the credit score before feeding the AI Agent.
Document upload
Add file upload fields to the Form Trigger for salary slips and bank statements. Store in S3 or Cloud Storage, then pass URLs to the AI Agent.
Applicant status portal
Build a status-check page querying Neon Postgres by application ID. Add the ID to confirmation emails so applicants can self-serve.
Security hardening
- Enable n8n webhook authentication
- Add rate limiting to the Form Trigger
- Store all credentials in n8n vault
- Enable Neon row-level security for multi-branch deployments
Ready to build your own AI screening agent?
Every node, prompt, and SQL query is documented above. Zero to working demo in an afternoon.
