Email Approval with Callback System
Learn how to implement email-based approval workflows using ReceiveTask and the callback system. Users click approve/reject links in emails to resume BPMN processes.
Overview
The Email Approval Pattern allows you to:
- ✅ Send approval requests via email with clickable buttons
- ✅ Pause BPMN process until user makes a decision
- ✅ Resume process automatically when user clicks approve/reject
- ✅ Support multi-channel approvals (email, SMS, in-app)
- ✅ Add security validation and timeout handling
How It Works
Complete Flow
[1. Generate Approval ID] → Unique correlation key created
↓
[2. Send Approval Email] → Email with approve/reject URLs sent
↓
[3. ReceiveTask] → Process pauses, waits for callback
↓
... User clicks button in email (30 min later) ...
↓
[4. Webhook Callback] → System receives approval decision
↓
[5. Process Resumes] → Continues with approval decision
↓
[6. Execute Next Tasks] → Based on approve/reject
Correlation Key Generation
Key Insight: Correlation key must be generated BEFORE sending email!
// ⌠WRONG: ReceiveTask generates key, but email already sent
[Send Email] → How do we know the correlation key?
↓
[ReceiveTask] → Key generated here (too late!)
// ✅ CORRECT: Generate key first, use in email and ReceiveTask
[Script Task] → Generate approvalId = "APPROVAL-LOAN123-1736544000"
↓
[Send Email] → Use ${approvalId} in email URLs
↓
[ReceiveTask] → Use ${approvalId} as correlationKey
BPMN Implementation
Complete Process Definition
<bpmn:process id="EmailApprovalProcess" name="Email-Based Approval">
<bpmn:startEvent id="Start" name="Approval Needed"/>
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<!-- STEP 1: Generate Unique Approval ID (Correlation Key) -->
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<bpmn:scriptTask id="GenerateApprovalId"
name="Generate Approval ID"
scriptFormat="javascript">
<bpmn:incoming>Start</bpmn:incoming>
<bpmn:outgoing>ToPrepareEmail</bpmn:outgoing>
<bpmn:script><![CDATA[
// Generate unique correlation key
context.approvalId = 'APPROVAL-' +
context.entityType + '-' +
context.entityId + '-' +
Date.now();
// Security token for validation
context.approvalToken = BankLingo.GenerateSecureToken(context.approvalId);
// Set expiry (48 hours)
context.approvalExpiresAt = BankLingo.AddHours(new Date(), 48);
console.log('Generated approvalId:', context.approvalId);
]]></bpmn:script>
</bpmn:scriptTask>
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<!-- STEP 2: Send Approval Email -->
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<bpmn:serviceTask id="SendApprovalEmail"
name="Send Approval Email"
implementation="SendApprovalEmailCommand">
<bpmn:incoming>ToPrepareEmail</bpmn:incoming>
<bpmn:outgoing>ToWaitForApproval</bpmn:outgoing>
<bpmn:extensionElements>
<custom:parameters>
<custom:parameter name="recipientEmail">${managerEmail}</custom:parameter>
<custom:parameter name="title">${approvalTitle}</custom:parameter>
<custom:parameter name="description">${approvalDescription}</custom:parameter>
<custom:parameter name="approvalId">${approvalId}</custom:parameter>
<custom:parameter name="approvalToken">${approvalToken}</custom:parameter>
<custom:parameter name="entityType">${entityType}</custom:parameter>
<custom:parameter name="entityId">${entityId}</custom:parameter>
<custom:parameter name="metadata">${customFields}</custom:parameter>
</custom:parameters>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<!-- STEP 3: Wait for Approval (ReceiveTask) -->
<!-- Uses EXISTING approvalId as correlation key -->
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<bpmn:receiveTask id="WaitForApproval"
name="Wait for Approval Decision">
<bpmn:incoming>ToWaitForApproval</bpmn:incoming>
<bpmn:outgoing>ToCheckDecision</bpmn:outgoing>
<bpmn:extensionElements>
<!-- USE PRE-GENERATED APPROVAL ID -->
<custom:correlationKey>${approvalId}</custom:correlationKey>
<custom:resultVariable>approvalResult</custom:resultVariable>
<custom:timeoutMinutes>2880</custom:timeoutMinutes> <!-- 48 hours -->
<custom:messageRef>ApprovalDecision</custom:messageRef>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Timeout handler -->
<bpmn:boundaryEvent id="ApprovalTimeout"
name="Approval Timeout"
attachedToRef="WaitForApproval">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT48H</bpmn:timeDuration>
</bpmn:timerEventDefinition>
<bpmn:outgoing>ToAutoReject</bpmn:outgoing>
</bpmn:boundaryEvent>
<bpmn:serviceTask id="AutoReject"
name="Auto-Reject (Timeout)"
implementation="RejectWithReasonCommand">
<bpmn:incoming>ToAutoReject</bpmn:incoming>
<bpmn:extensionElements>
<custom:parameters>
<custom:parameter name="reason">Approval timeout - no response within 48 hours</custom:parameter>
</custom:parameters>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<!-- STEP 4: Route Based on Decision -->
<!-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• -->
<bpmn:exclusiveGateway id="CheckDecision" name="Approved?">
<bpmn:incoming>ToCheckDecision</bpmn:incoming>
<bpmn:outgoing sequenceFlow="${approvalResult.approved == true}">ToApproved</bpmn:outgoing>
<bpmn:outgoing sequenceFlow="${approvalResult.approved == false}">ToRejected</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="EndApproved" name="Approved"/>
<bpmn:endEvent id="EndRejected" name="Rejected"/>
</bpmn:process>
Backend Implementation
1. Send Approval Email Command
Implementation details removed for security.
Contact support for implementation guidance.
2. Approval Callback Controller
Implementation details removed for security.
Contact support for implementation guidance.
3. Resume Process Command
Implementation details removed for security.
Contact support for implementation guidance.
Usage Examples
Loan Approval
Implementation details removed for security.
Contact support for implementation guidance.
High-Value Transaction Approval
Implementation details removed for security.
Contact support for implementation guidance.
Key Insights
✅ Correlation Key Generation
When: Generated in Script Task BEFORE sending email
Why: Email needs the correlation key to build callback URLs
How: Stored in process variables, used by both email task and ReceiveTask
// Generate ONCE
context.approvalId = 'APPROVAL-' + context.entityId + '-' + Date.now();
// Email task uses it
SendApprovalEmailCommand { ApprovalId = ${approvalId} }
// ReceiveTask uses same key
<custom:correlationKey>${approvalId}</custom:correlationKey>
✅ Callback Flow
1. User clicks button → GET /api/approval/callback?key=APPROVAL-...&decision=approved
2. Controller validates token and expiry
3. Controller finds CallbackRegistry by correlation key
4. Controller calls ResumeProcessByCallbackCommand
5. Process state loaded from CallbackRegistry.ParentProcessState
6. Approval decision stored in process variables
7. Process resumes from ReceiveTask
8. Gateway routes based on decision
✅ Security
- Token validation: SHA256 hash prevents tampering
- One-time use: Callback marked as "Completed" after first use
- Expiry check: Timeout boundary event handles no-response
- HTTPS only: All URLs must use HTTPS in production
Best Practices
- Generate correlation key early - Before sending any notifications
- Use security tokens - Always validate tokens in webhook
- Handle timeouts - Use boundary events for expired approvals
- Store metadata - Include all context needed for approval in email
- Audit trail - Log all approval decisions with timestamps
- One-time use - Mark callbacks as completed to prevent replay attacks
- User-friendly messages - Show clear success/error pages after callback
Related Documentation
You now have a production-ready email approval system! 🎉