Complete Example: Loan Approval Process
A comprehensive loan approval workflow demonstrating validation, user approvals, dynamic routing, and notifications.
Process Overview
Business Scenario
A customer submits a loan application. The system validates the application, performs risk assessment, routes to the appropriate approval level, and notifies the customer of the decision.
Process Flow
Start
↓
Validate Application (Script)
↓
Calculate Risk Score (Script)
↓
Risk Assessment Gateway
├── Low Risk → Auto-Approve
├── Medium Risk → Loan Officer Review (User)
├── High Risk → Manager Review (User)
└── Very High Risk → Reject
↓
Approval Decision Gateway
├── Approved → Send Approval Notification (Send)
└── Rejected → Send Rejection Notification (Send)
↓
End
Features Demonstrated
- ✅ ScriptTask - Data validation and calculations
- ✅ UserTask - Human approvals
- ✅ Gateway - Dynamic routing based on risk
- ✅ SendTask - Email notifications
- ✅ Variables - Data flow throughout process
- ✅ Conditional Routing - Multi-path decisions
Complete BPMN XML
Save this as loan-approval-complete.bpmn:
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:custom="http://banklingo.com/schema/bpmn"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
targetNamespace="http://banklingo.com/bpmn"
id="Definitions_LoanApproval">
<bpmn:process id="LoanApprovalComplete" name="Complete Loan Approval Process" isExecutable="true">
<!-- ========== START EVENT ========== -->
<bpmn:startEvent id="StartEvent_1" name="Loan Application Received" />
<bpmn:sequenceFlow sourceRef="StartEvent_1" targetRef="Task_ValidateApplication" />
<!-- ========== TASK 1: VALIDATE APPLICATION ========== -->
<bpmn:scriptTask id="Task_ValidateApplication" name="Validate Application Data">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Validate required fields
const errors = [];
if (!loanAmount || loanAmount <= 0) {
errors.push('Invalid loan amount');
}
if (loanAmount > 500000) {
errors.push('Loan amount exceeds maximum of $500,000');
}
if (!customerName || customerName.trim() === '') {
errors.push('Customer name is required');
}
if (!customerEmail || !customerEmail.includes('@')) {
errors.push('Valid email is required');
}
if (!loanPurpose || loanPurpose.trim() === '') {
errors.push('Loan purpose is required');
}
if (!termMonths || termMonths < 12 || termMonths > 360) {
errors.push('Term must be between 12 and 360 months');
}
// Check if validation passed
if (errors.length > 0) {
throw new Error('Validation failed: ' + errors.join(', '));
}
// Set validation metadata
validationStatus = 'Passed';
validationDate = new Date().toISOString();
validationErrors = [];
console.log('Application validated successfully');
" />
<custom:property name="ScriptLanguage" value="javascript" />
<custom:property name="EntityState" value="Validating" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>StartEvent_1</bpmn:incoming>
<bpmn:outgoing>Flow_ToRiskCalculation</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_ToRiskCalculation" sourceRef="Task_ValidateApplication" targetRef="Task_CalculateRisk" />
<!-- ========== TASK 2: CALCULATE RISK SCORE ========== -->
<bpmn:scriptTask id="Task_CalculateRisk" name="Calculate Risk Score">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
// Calculate risk score based on multiple factors
let riskScore = 0;
// Factor 1: Loan amount (0-30 points)
if (loanAmount < 10000) riskScore += 5;
else if (loanAmount < 50000) riskScore += 10;
else if (loanAmount < 100000) riskScore += 20;
else riskScore += 30;
// Factor 2: Term length (0-20 points)
if (termMonths <= 36) riskScore += 5;
else if (termMonths <= 60) riskScore += 10;
else if (termMonths <= 120) riskScore += 15;
else riskScore += 20;
// Factor 3: Credit score (if provided) (0-50 points)
if (creditScore) {
if (creditScore < 580) riskScore += 50;
else if (creditScore < 640) riskScore += 40;
else if (creditScore < 680) riskScore += 25;
else if (creditScore < 720) riskScore += 15;
else riskScore += 5;
} else {
// No credit score = higher risk
riskScore += 30;
}
// Calculate risk level
if (riskScore < 30) {
riskLevel = 'Low';
autoApproveEligible = true;
} else if (riskScore < 50) {
riskLevel = 'Medium';
autoApproveEligible = false;
} else if (riskScore < 70) {
riskLevel = 'High';
autoApproveEligible = false;
} else {
riskLevel = 'VeryHigh';
autoApproveEligible = false;
}
// Calculate estimated interest rate
baseRate = 5.0;
if (riskLevel === 'Low') estimatedRate = baseRate + 1.0;
else if (riskLevel === 'Medium') estimatedRate = baseRate + 2.5;
else if (riskLevel === 'High') estimatedRate = baseRate + 4.0;
else estimatedRate = baseRate + 6.0;
// Calculate monthly payment
const monthlyRate = estimatedRate / 12 / 100;
monthlyPayment = loanAmount *
(monthlyRate * Math.pow(1 + monthlyRate, termMonths)) /
(Math.pow(1 + monthlyRate, termMonths) - 1);
monthlyPayment = Math.round(monthlyPayment * 100) / 100;
riskCalculatedAt = new Date().toISOString();
console.log('Risk Score:', riskScore, 'Level:', riskLevel);
" />
<custom:property name="EntityState" value="RiskAssessment" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ToRiskCalculation</bpmn:incoming>
<bpmn:outgoing>Flow_ToRiskGateway</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_ToRiskGateway" sourceRef="Task_CalculateRisk" targetRef="Gateway_RiskRouting" />
<!-- ========== GATEWAY: RISK-BASED ROUTING ========== -->
<bpmn:exclusiveGateway id="Gateway_RiskRouting" name="Risk Assessment" default="Flow_Reject">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Condition" value="
// Route based on risk level
if (riskLevel === 'Low' && autoApproveEligible) {
return 'Flow_AutoApprove';
} else if (riskLevel === 'Medium') {
return 'Flow_OfficerReview';
} else if (riskLevel === 'High') {
return 'Flow_ManagerReview';
}
// Very high risk goes to rejection
return 'Flow_Reject';
" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ToRiskGateway</bpmn:incoming>
<bpmn:outgoing>Flow_AutoApprove</bpmn:outgoing>
<bpmn:outgoing>Flow_OfficerReview</bpmn:outgoing>
<bpmn:outgoing>Flow_ManagerReview</bpmn:outgoing>
<bpmn:outgoing>Flow_Reject</bpmn:outgoing>
</bpmn:exclusiveGateway>
<!-- ========== PATH 1: AUTO-APPROVE (LOW RISK) ========== -->
<bpmn:sequenceFlow id="Flow_AutoApprove" name="Low Risk - Auto-Approve"
sourceRef="Gateway_RiskRouting" targetRef="Task_SetApproved" />
<bpmn:scriptTask id="Task_SetApproved" name="Set Auto-Approved">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
approvalDecision = 'Approved';
approvalMethod = 'Automatic';
approvedBy = 'System';
approvalDate = new Date().toISOString();
approvalNotes = 'Auto-approved based on low risk assessment';
" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_AutoApprove</bpmn:incoming>
<bpmn:outgoing>Flow_AutoToNotify</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_AutoToNotify" sourceRef="Task_SetApproved" targetRef="Gateway_DecisionMerge" />
<!-- ========== PATH 2: LOAN OFFICER REVIEW (MEDIUM RISK) ========== -->
<bpmn:sequenceFlow id="Flow_OfficerReview" name="Medium Risk - Officer Review"
sourceRef="Gateway_RiskRouting" targetRef="Task_OfficerReview" />
<bpmn:userTask id="Task_OfficerReview" name="Loan Officer Review">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="loan-officer-review" />
<custom:property name="UserActions" value="Approve,Reject,RequestMoreInfo" />
<custom:property name="EntityState" value="PendingOfficerReview" />
<custom:property name="ResponsibleTeams" value="LoanOfficers" />
<custom:property name="Description" value="Review medium-risk loan application and make approval decision" />
<custom:property name="InputMapping" value="{
"application": "{customerName, loanAmount, loanPurpose, termMonths}",
"risk": "{riskLevel, riskScore, estimatedRate}",
"payment": "monthlyPayment"
}" />
<custom:property name="OutputMapping" value="{
"approvalDecision": "decision",
"approverComments": "comments",
"approvedBy": "officerName"
}" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_OfficerReview</bpmn:incoming>
<bpmn:outgoing>Flow_OfficerToMerge</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_OfficerToMerge" sourceRef="Task_OfficerReview" targetRef="Gateway_DecisionMerge" />
<!-- ========== PATH 3: MANAGER REVIEW (HIGH RISK) ========== -->
<bpmn:sequenceFlow id="Flow_ManagerReview" name="High Risk - Manager Review"
sourceRef="Gateway_RiskRouting" targetRef="Task_ManagerReview" />
<bpmn:userTask id="Task_ManagerReview" name="Manager Review (High Risk)">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="manager-review" />
<custom:property name="UserActions" value="Approve,Reject,EscalateToBoard" />
<custom:property name="EntityState" value="PendingManagerReview" />
<custom:property name="ResponsibleTeams" value="Managers,SeniorManagers" />
<custom:property name="Description" value="High-risk loan application requiring manager approval" />
<custom:property name="InputMapping" value="{
"application": "{customerName, loanAmount, loanPurpose, termMonths}",
"risk": "{riskLevel, riskScore, estimatedRate}",
"payment": "monthlyPayment"
}" />
<custom:property name="OutputMapping" value="{
"approvalDecision": "decision",
"approverComments": "comments",
"approvedBy": "managerName"
}" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ManagerReview</bpmn:incoming>
<bpmn:outgoing>Flow_ManagerToMerge</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_ManagerToMerge" sourceRef="Task_ManagerReview" targetRef="Gateway_DecisionMerge" />
<!-- ========== PATH 4: AUTO-REJECT (VERY HIGH RISK) ========== -->
<bpmn:sequenceFlow id="Flow_Reject" name="Very High Risk - Reject"
sourceRef="Gateway_RiskRouting" targetRef="Task_SetRejected" />
<bpmn:scriptTask id="Task_SetRejected" name="Set Auto-Rejected">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Script" value="
approvalDecision = 'Rejected';
approvalMethod = 'Automatic';
approvedBy = 'System';
approvalDate = new Date().toISOString();
rejectionReason = 'Application does not meet minimum risk criteria';
approvalNotes = 'Auto-rejected due to very high risk assessment';
" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_Reject</bpmn:incoming>
<bpmn:outgoing>Flow_RejectToMerge</bpmn:outgoing>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_RejectToMerge" sourceRef="Task_SetRejected" targetRef="Gateway_DecisionMerge" />
<!-- ========== MERGE GATEWAY ========== -->
<bpmn:exclusiveGateway id="Gateway_DecisionMerge" name="Merge Paths" />
<bpmn:incoming>Flow_AutoToNotify</bpmn:incoming>
<bpmn:incoming>Flow_OfficerToMerge</bpmn:incoming>
<bpmn:incoming>Flow_ManagerToMerge</bpmn:incoming>
<bpmn:incoming>Flow_RejectToMerge</bpmn:incoming>
<bpmn:outgoing>Flow_ToFinalDecision</bpmn:outgoing>
<bpmn:sequenceFlow id="Flow_ToFinalDecision" sourceRef="Gateway_DecisionMerge" targetRef="Gateway_FinalDecision" />
<!-- ========== GATEWAY: FINAL DECISION ROUTING ========== -->
<bpmn:exclusiveGateway id="Gateway_FinalDecision" name="Approved?" default="Flow_ToRejectionNotification">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Condition" value="
return approvalDecision === 'Approved' ? 'Flow_ToApprovalNotification' : 'Flow_ToRejectionNotification';
" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ToFinalDecision</bpmn:incoming>
<bpmn:outgoing>Flow_ToApprovalNotification</bpmn:outgoing>
<bpmn:outgoing>Flow_ToRejectionNotification</bpmn:outgoing>
</bpmn:exclusiveGateway>
<!-- ========== APPROVED PATH: SEND NOTIFICATION ========== -->
<bpmn:sequenceFlow id="Flow_ToApprovalNotification" name="Approved"
sourceRef="Gateway_FinalDecision" targetRef="Task_SendApprovalEmail" />
<bpmn:sendTask id="Task_SendApprovalEmail" name="Send Approval Notification">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Destination" value="/api/notifications/send" />
<custom:property name="Method" value="POST" />
<custom:property name="PayloadTemplate" value="{
"recipientEmail": "{{customerEmail}}",
"recipientName": "{{customerName}}",
"subject": "Loan Application Approved",
"templateId": "loan-approval",
"templateData": {
"customerName": "{{customerName}}",
"loanAmount": "{{loanAmount}}",
"interestRate": "{{estimatedRate}}",
"monthlyPayment": "{{monthlyPayment}}",
"termMonths": "{{termMonths}}",
"approvedBy": "{{approvedBy}}",
"nextSteps": "Please visit your nearest branch to complete the loan documentation."
},
"priority": "high"
}" />
<custom:property name="Retries" value="3" />
<custom:property name="TimeoutMs" value="10000" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ToApprovalNotification</bpmn:incoming>
<bpmn:outgoing>Flow_ToApprovedEnd</bpmn:outgoing>
</bpmn:sendTask>
<bpmn:sequenceFlow id="Flow_ToApprovedEnd" sourceRef="Task_SendApprovalEmail" targetRef="EndEvent_Approved" />
<bpmn:endEvent id="EndEvent_Approved" name="Application Approved" />
<!-- ========== REJECTED PATH: SEND NOTIFICATION ========== -->
<bpmn:sequenceFlow id="Flow_ToRejectionNotification" name="Rejected"
sourceRef="Gateway_FinalDecision" targetRef="Task_SendRejectionEmail" />
<bpmn:sendTask id="Task_SendRejectionEmail" name="Send Rejection Notification">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Destination" value="/api/notifications/send" />
<custom:property name="Method" value="POST" />
<custom:property name="PayloadTemplate" value="{
"recipientEmail": "{{customerEmail}}",
"recipientName": "{{customerName}}",
"subject": "Loan Application Decision",
"templateId": "loan-rejection",
"templateData": {
"customerName": "{{customerName}}",
"loanAmount": "{{loanAmount}}",
"rejectionReason": "{{rejectionReason}}",
"nextSteps": "You may reapply after addressing the concerns mentioned."
},
"priority": "normal"
}" />
<custom:property name="Retries" value="3" />
<custom:property name="TimeoutMs" value="10000" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ToRejectionNotification</bpmn:incoming>
<bpmn:outgoing>Flow_ToRejectedEnd</bpmn:outgoing>
</bpmn:sendTask>
<bpmn:sequenceFlow id="Flow_ToRejectedEnd" sourceRef="Task_SendRejectionEmail" targetRef="EndEvent_Rejected" />
<bpmn:endEvent id="EndEvent_Rejected" name="Application Rejected" />
</bpmn:process>
</bpmn:definitions>
Deployment
1. Deploy the Process Definition
POST /api/process-definitions
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
{
"processKey": "LoanApprovalComplete",
"name": "Complete Loan Approval Process",
"description": "End-to-end loan approval with validation, risk assessment, and notifications",
"bpmnXml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>...",
"version": 1,
"isActive": true
}
Execution Examples
Example 1: Low Risk - Auto-Approved
Start Process:
{
"processKey": "LoanApprovalComplete",
"businessKey": "LOAN-2025-001",
"variables": {
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"loanAmount": 8000,
"loanPurpose": "Home Improvement",
"termMonths": 24,
"creditScore": 750
}
}
Expected Flow:
- Validation ✓
- Risk Calculation → riskLevel: "Low", riskScore: 20
- Gateway → Auto-Approve path
- Set Approved
- Send Approval Email
- End (Approved)
No user interaction needed!
Example 2: Medium Risk - Officer Review
Start Process:
{
"processKey": "LoanApprovalComplete",
"businessKey": "LOAN-2025-002",
"variables": {
"customerName": "Jane Smith",
"customerEmail": "jane.smith@example.com",
"loanAmount": 45000,
"loanPurpose": "Business Expansion",
"termMonths": 60,
"creditScore": 680
}
}
Expected Flow:
- Validation ✓
- Risk Calculation → riskLevel: "Medium", riskScore: 40
- Gateway → Officer Review path
- PAUSES at Task_OfficerReview (UserTask)
Complete UserTask:
POST /api/user-tasks/complete
{
"instanceGuid": "{guid}",
"taskId": "Task_OfficerReview",
"userAction": "Approve",
"variables": {
"decision": "Approved",
"comments": "Strong business plan. Approved with standard terms.",
"officerName": "Mike Johnson"
}
}
Continues: 5. Gateway → Approved path 6. Send Approval Email 7. End (Approved)
Example 3: High Risk - Manager Review
Start Process:
{
"processKey": "LoanApprovalComplete",
"businessKey": "LOAN-2025-003",
"variables": {
"customerName": "Bob Wilson",
"customerEmail": "bob.wilson@example.com",
"loanAmount": 150000,
"loanPurpose": "Real Estate Investment",
"termMonths": 180,
"creditScore": 620
}
}
Expected Flow:
- Validation ✓
- Risk Calculation → riskLevel: "High", riskScore: 65
- Gateway → Manager Review path
- PAUSES at Task_ManagerReview (UserTask)
Complete UserTask:
POST /api/user-tasks/complete
{
"instanceGuid": "{guid}",
"taskId": "Task_ManagerReview",
"userAction": "Approve",
"variables": {
"decision": "Approved",
"comments": "Approved with higher interest rate and additional collateral requirement.",
"managerName": "Sarah Lee"
}
}
Continues: 5. Gateway → Approved path 6. Send Approval Email 7. End (Approved)
Example 4: Very High Risk - Auto-Rejected
Start Process:
{
"processKey": "LoanApprovalComplete",
"businessKey": "LOAN-2025-004",
"variables": {
"customerName": "Alex Brown",
"customerEmail": "alex.brown@example.com",
"loanAmount": 300000,
"loanPurpose": "Investment",
"termMonths": 240,
"creditScore": 550
}
}
Expected Flow:
- Validation ✓
- Risk Calculation → riskLevel: "VeryHigh", riskScore: 80
- Gateway → Reject path
- Set Rejected
- Send Rejection Email
- End (Rejected)
No user interaction needed!
Variables Throughout Process
Initial Variables (Input)
{
"customerName": "John Doe",
"customerEmail": "john.doe@example.com",
"loanAmount": 45000,
"loanPurpose": "Business Expansion",
"termMonths": 60,
"creditScore": 680
}
After Validation
{
...initial,
"validationStatus": "Passed",
"validationDate": "2025-12-18T10:00:00Z",
"validationErrors": []
}
After Risk Calculation
{
...previous,
"riskScore": 40,
"riskLevel": "Medium",
"autoApproveEligible": false,
"estimatedRate": 7.5,
"monthlyPayment": 891.33,
"riskCalculatedAt": "2025-12-18T10:00:01Z"
}
After Approval (UserTask)
{
...previous,
"approvalDecision": "Approved",
"approverComments": "Strong business plan",
"approvedBy": "Mike Johnson",
"approvalDate": "2025-12-18T10:15:00Z",
"approvalMethod": "Manual"
}
Final Variables
{
...all_previous,
"notificationSent": true,
"notificationSentAt": "2025-12-18T10:15:05Z",
"processCompletedAt": "2025-12-18T10:15:05Z"
}
Customization Ideas
1. Add Parallel Credit Checks
Replace risk calculation with parallel checks:
<bpmn:parallelGateway id="Gateway_ParallelChecks" />
<!-- Add multiple ServiceTasks for credit bureaus -->
<bpmn:parallelGateway id="Gateway_JoinChecks" />
2. Add Document Upload Requirements
Insert after approval, before notification:
<bpmn:userTask id="Task_UploadDocuments" name="Upload Required Documents">
<custom:property name="FormKey" value="document-upload" />
</bpmn:userTask>
3. Add SLA Timers
Add boundary timer events for escalation:
<bpmn:boundaryEvent id="Event_ReviewTimeout" attachedToRef="Task_OfficerReview">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT24H</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
4. Integrate with External Systems
Replace script-based risk calculation with API call:
<bpmn:serviceTask id="Task_CreditBureauCheck" name="Credit Bureau Check">
<custom:property name="Endpoint" value="https://credit-bureau-api.com/check" />
<custom:property name="Method" value="POST" />
</bpmn:serviceTask>
5. Add SMS Notifications
Add parallel notification tasks:
<bpmn:parallelGateway id="Gateway_NotifySplit" />
<bpmn:sendTask id="Task_SendEmail" ... />
<bpmn:sendTask id="Task_SendSMS" ... />
<bpmn:parallelGateway id="Gateway_NotifyJoin" />
Testing
Test in Supervised Mode
{
"processKey": "LoanApprovalComplete",
"businessKey": "TEST-001",
"variables": { ... },
"executionMode": "Supervised"
}
Then use step-forward to verify each task executes correctly.
Test Different Risk Levels
| Credit Score | Loan Amount | Expected Risk | Expected Path |
|---|---|---|---|
| 750+ | < $10,000 | Low | Auto-Approve |
| 680-720 | $25,000-$75,000 | Medium | Officer Review |
| 620-680 | $100,000+ | High | Manager Review |
| < 580 | Any | Very High | Auto-Reject |
Next Steps
- Customer Onboarding Example - More complex workflow
- Payment Authorization Example - Real-time decisions
- Task Types - Deep dive into each task type
- API Reference - Complete API documentation