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