Callbacks and External Signals (Phase 3)
Callbacks enable process workflows to pause and wait for external events, such as webhook callbacks, payment confirmations, or third-party API responses.
Overview
Callback functionality provides:
- ✅ External event handling - Wait for webhooks and API callbacks
- ✅ Automatic process resumption - Process continues when event arrives
- ✅ Correlation by key - Match callbacks to correct process instance
- ✅ Timeout support - Handle missed callbacks with timers
- ✅ Persistent state - Callbacks survive server restarts
How It Works
Architecture
1. Process reaches Receive Task
↓
2. Process pauses, waits for external signal
↓
3. Callback registered in database (CallbackRegistry table)
↓
4. External system sends webhook/API call
↓
5. Callback endpoint receives request
↓
6. Process resumes automatically with callback data
CallbackRegistry Table
Implementation details removed for security.
Contact support for implementation guidance.
Receive Task Configuration
Use Receive Task to wait for external signals:
<bpmn:receiveTask id="WaitForPayment" name="Wait for Payment Callback">
<bpmn:extensionElements>
<custom:properties>
<!-- Correlation key to match callback -->
<custom:property name="CorrelationKey" value="${context.orderId}"/>
<!-- Callback type -->
<custom:property name="CallbackType" value="payment"/>
<!-- Expected source (optional) -->
<custom:property name="ExpectedSource" value="PaymentGateway"/>
<!-- Timeout (optional) -->
<custom:property name="TimeoutDuration" value="PT30M"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
Common Patterns
Pattern 1: Payment Gateway Callback
Process waits for payment confirmation from external gateway.
Process Definition
<bpmn:startEvent id="StartCheckout" name="Start"/>
<!-- Initiate payment -->
<bpmn:serviceTask id="InitiatePayment" name="Initiate Payment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="InitiatePaymentCommand"/>
<custom:property name="amount" value="${context.amount}"/>
<custom:property name="currency" value="NGN"/>
<custom:property name="customerEmail" value="${context.customerEmail}"/>
<custom:property name="callbackUrl" value="https://api.bank.com/callbacks/payment"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PrepareCallback" name="Prepare Callback">
<bpmn:script>
// Payment gateway returns reference
var paymentRef = context.paymentReference;
context.orderId = 'ORD-' + new Date().getTime();
context.correlationKey = paymentRef; // Use payment reference as correlation key
logger.info('Waiting for payment callback: ' + paymentRef);
return {
orderId: context.orderId,
paymentReference: paymentRef,
status: 'PENDING_PAYMENT'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR CALLBACK: Process pauses here -->
<bpmn:receiveTask id="WaitForPaymentCallback" name="Wait for Payment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="payment"/>
<custom:property name="TimeoutDuration" value="PT30M"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Timeout boundary (if payment not received in 30 minutes) -->
<bpmn:boundaryEvent id="PaymentTimeout"
attachedToRef="WaitForPaymentCallback"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT30M</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Handle timeout -->
<bpmn:scriptTask id="HandleTimeout" name="Handle Timeout">
<bpmn:script>
logger.warn('Payment timeout for order ' + context.orderId);
context.paymentStatus = 'TIMEOUT';
context.orderStatus = 'CANCELLED';
// Cancel payment at gateway
BankLingo.ExecuteCommand('CancelPayment', {
paymentReference: context.paymentReference
});
return {
status: 'CANCELLED',
reason: 'PAYMENT_TIMEOUT'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Process payment result -->
<bpmn:scriptTask id="ProcessPaymentResult" name="Process Result">
<bpmn:script>
// Callback data available in context
var callbackData = context.callbackData;
context.paymentStatus = callbackData.status; // SUCCESS, FAILED
context.transactionId = callbackData.transactionId;
context.paidAmount = callbackData.amount;
logger.info('Payment received: ' + callbackData.status +
', amount: ' + callbackData.amount);
return {
paymentStatus: context.paymentStatus,
transactionId: context.transactionId
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: Payment successful? -->
<bpmn:exclusiveGateway id="PaymentSuccessful" name="Success?"/>
<bpmn:sequenceFlow sourceRef="PaymentSuccessful" targetRef="FulfillOrder">
<bpmn:conditionExpression>${context.paymentStatus === 'SUCCESS'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="PaymentSuccessful" targetRef="HandleFailedPayment">
<bpmn:conditionExpression>${context.paymentStatus === 'FAILED'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Fulfill order -->
<bpmn:serviceTask id="FulfillOrder" name="Fulfill Order">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FulfillOrderCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:endEvent id="End"/>
Callback Endpoint (API)
Implementation details removed for security.
Contact support for implementation guidance.
Timeline:
- User initiates payment
- Payment gateway returns reference (e.g., "PYR-123456")
- Process waits at Receive Task with correlation key "PYR-123456"
- User completes payment on gateway
- Gateway sends webhook to callback URL
- API matches correlation key and resumes process
- Process continues with payment result
Pattern 2: Third-Party KYC Verification
Wait for KYC verification results from external provider.
<bpmn:startEvent id="StartKYC" name="Start"/>
<!-- Submit KYC request -->
<bpmn:serviceTask id="SubmitKYCRequest" name="Submit KYC">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="SubmitKYCRequestCommand"/>
<custom:property name="customerId" value="${context.customerId}"/>
<custom:property name="callbackUrl" value="https://api.bank.com/callbacks/kyc"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PrepareKYCCallback" name="Prepare">
<bpmn:script>
// KYC provider returns request ID
context.kycRequestId = context.kycResponse.requestId;
context.correlationKey = context.kycRequestId;
logger.info('Waiting for KYC callback: ' + context.kycRequestId);
return {
kycRequestId: context.kycRequestId,
status: 'KYC_PENDING'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR KYC RESULT: Can take hours or days -->
<bpmn:receiveTask id="WaitForKYCResult" name="Wait for KYC Result">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="kyc"/>
<custom:property name="TimeoutDuration" value="P7D"/> <!-- 7 days -->
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Timeout boundary (7 days) -->
<bpmn:boundaryEvent id="KYCTimeout"
attachedToRef="WaitForKYCResult"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P7D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Timeout handling -->
<bpmn:userTask id="ManualKYCReview" name="Manual KYC Review">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="manual-kyc-form"/>
<custom:property name="Description" value="KYC provider did not respond within 7 days"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Process KYC result -->
<bpmn:scriptTask id="ProcessKYCResult" name="Process Result">
<bpmn:script>
var kycData = context.callbackData;
context.kycStatus = kycData.status; // VERIFIED, REJECTED, REVIEW_REQUIRED
context.verificationLevel = kycData.verificationLevel;
context.matchScore = kycData.matchScore;
logger.info('KYC result: ' + context.kycStatus + ', score: ' + context.matchScore);
return {
kycStatus: context.kycStatus,
verificationLevel: context.verificationLevel
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: KYC verified? -->
<bpmn:exclusiveGateway id="KYCVerified" name="Verified?"/>
<bpmn:sequenceFlow sourceRef="KYCVerified" targetRef="ActivateAccount">
<bpmn:conditionExpression>${context.kycStatus === 'VERIFIED'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="KYCVerified" targetRef="RejectApplication">
<bpmn:conditionExpression>${context.kycStatus === 'REJECTED'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:endEvent id="End"/>
Use Cases:
- Identity verification (KYC/AML)
- Document verification services
- Credit bureau checks
- Background verification
Pattern 3: Webhook Integration
Generic webhook handling for any external system.
<bpmn:startEvent id="StartWebhookProcess" name="Start"/>
<!-- Register webhook -->
<bpmn:scriptTask id="RegisterWebhook" name="Register Webhook">
<bpmn:script>
// Generate unique webhook ID
context.webhookId = 'WH-' + generateUUID();
context.webhookUrl = 'https://api.bank.com/webhooks/' + context.webhookId;
context.correlationKey = context.webhookId;
// Register webhook with external system
BankLingo.ExecuteCommand('RegisterWebhook', {
webhookId: context.webhookId,
url: context.webhookUrl,
events: ['order.completed', 'order.cancelled'],
secret: context.webhookSecret
});
logger.info('Webhook registered: ' + context.webhookUrl);
return {
webhookId: context.webhookId,
webhookUrl: context.webhookUrl
};
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0;
var v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR WEBHOOK -->
<bpmn:receiveTask id="WaitForWebhook" name="Wait for Webhook">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="webhook"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Process webhook data -->
<bpmn:scriptTask id="ProcessWebhookData" name="Process Data">
<bpmn:script>
var webhookData = context.callbackData;
context.eventType = webhookData.event;
context.eventData = webhookData.data;
logger.info('Webhook received: ' + context.eventType);
return {
eventType: context.eventType
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: Event type? -->
<bpmn:exclusiveGateway id="EventType" name="Event Type?"/>
<bpmn:sequenceFlow sourceRef="EventType" targetRef="HandleOrderCompleted">
<bpmn:conditionExpression>${context.eventType === 'order.completed'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="EventType" targetRef="HandleOrderCancelled">
<bpmn:conditionExpression>${context.eventType === 'order.cancelled'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:endEvent id="End"/>
Pattern 4: Multiple Callbacks with Subprocess
Wait for multiple callbacks using subprocess instances.
<bpmn:startEvent id="StartMultiCheck" name="Start"/>
<!-- Define services to check -->
<bpmn:scriptTask id="PrepareServiceChecks" name="Prepare Checks">
<bpmn:script>
context.services = [
{ name: 'CreditBureau', type: 'credit_check' },
{ name: 'IdentityProvider', type: 'identity_check' },
{ name: 'FraudDetection', type: 'fraud_check' }
];
return {
serviceCount: context.services.length
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Call multiple services (multi-instance) -->
<bpmn:callActivity id="CallServices"
name="Call Services"
calledElement="CallSingleService">
<bpmn:multiInstanceLoopCharacteristics isSequential="false">
<custom:property name="collection" value="context.services"/>
<custom:property name="elementVariable" value="currentService"/>
<custom:property name="aggregationVariable" value="serviceResults"/>
</bpmn:multiInstanceLoopCharacteristics>
</bpmn:callActivity>
<!-- Subprocess: Call single service and wait for callback -->
<bpmn:process id="CallSingleService">
<bpmn:startEvent id="SubStart"/>
<!-- Initiate check -->
<bpmn:serviceTask id="InitiateCheck" name="Initiate Check">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="InitiateExternalCheckCommand"/>
<custom:property name="serviceName" value="${context.currentService.name}"/>
<custom:property name="checkType" value="${context.currentService.type}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PrepareSubprocessCallback" name="Prepare">
<bpmn:script>
context.checkId = context.externalCheckResponse.checkId;
context.subprocessCorrelationKey = context.checkId;
return {
checkId: context.checkId
};
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR SERVICE CALLBACK -->
<bpmn:receiveTask id="WaitForCheckResult" name="Wait for Result">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.subprocessCorrelationKey}"/>
<custom:property name="CallbackType" value="${context.currentService.type}"/>
<custom:property name="TimeoutDuration" value="PT10M"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Return result -->
<bpmn:scriptTask id="ReturnResult" name="Return Result">
<bpmn:script>
var result = context.callbackData;
return {
serviceName: context.currentService.name,
checkType: context.currentService.type,
status: result.status,
score: result.score,
details: result.details
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="SubEnd"/>
</bpmn:process>
<!-- Aggregate all results -->
<bpmn:scriptTask id="AggregateResults" name="Aggregate Results">
<bpmn:script>
var results = context.serviceResults;
var allPassed = results.every(r => r.status === 'PASSED');
var anyFailed = results.some(r => r.status === 'FAILED');
context.overallStatus = allPassed ? 'APPROVED' :
anyFailed ? 'REJECTED' : 'REVIEW';
logger.info('All checks complete: ' + context.overallStatus);
return {
overallStatus: context.overallStatus,
results: results
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="End"/>
Benefits:
- ✅ All 3 services called in parallel
- ✅ Each waits for its own callback independently
- ✅ Parent process continues when all complete
- ✅ Efficient parallel external service integration
Callback Security
Webhook Signature Verification
Implementation details removed for security.
Contact support for implementation guidance.
IP Whitelisting
Implementation details removed for security.
Contact support for implementation guidance.
Best Practices
✅ Do This
<!-- ✅ Use unique correlation keys -->
<custom:property name="CorrelationKey" value="${context.paymentReference}"/>
<!-- ✅ Set timeouts for callbacks -->
<custom:property name="TimeoutDuration" value="PT30M"/>
<!-- ✅ Add timeout boundaries -->
<bpmn:boundaryEvent attachedToRef="WaitForCallback" cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT30M</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- ✅ Validate callback data -->
if (!callbackData.status) {
throw new BpmnError('INVALID_CALLBACK', 'Missing status');
}
<!-- ✅ Log callback events -->
logger.info('Callback received: ' + correlationKey);
⌠Don't Do This
<!-- ⌠No correlation key -->
<bpmn:receiveTask id="Wait"/> <!-- How to match callback? -->
<!-- ⌠No timeout -->
<bpmn:receiveTask id="Wait"/> <!-- Could wait forever! -->
<!-- ⌠Predictable correlation keys -->
<custom:property name="CorrelationKey" value="customer-${context.customerId}"/>
<!-- Security risk! -->
<!-- ⌠No error handling -->
<!-- What if callback never arrives? -->
Related Documentation
- Receive Task - Receive task fundamentals
- Callback Patterns - Advanced callback patterns
- Timer Events - Callback timeouts
- Error Handling - Callback error handling
Features Used:
- Phase 3: Callbacks
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026