Skip to main content

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​

Code Removed

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)​

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Timeline:

  1. User initiates payment
  2. Payment gateway returns reference (e.g., "PYR-123456")
  3. Process waits at Receive Task with correlation key "PYR-123456"
  4. User completes payment on gateway
  5. Gateway sends webhook to callback URL
  6. API matches correlation key and resumes process
  7. 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​

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

IP Whitelisting​

Code Removed

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? -->

Features Used:

  • Phase 3: Callbacks

Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026