Skip to main content

Boundary Error Events

Boundary error events are BPMN elements that catch errors thrown by tasks, enabling controlled error handling and recovery strategies.

Overview​

Boundary events allow you to:

  • ✅ Catch specific errors by error code
  • ✅ Choose interrupting or non-interrupting behavior
  • ✅ Implement recovery logic for different error scenarios
  • ✅ Prevent process crashes from unhandled errors

Boundary Event Types​

Interrupting Boundary Events​

Interrupting boundary events cancel the task when an error occurs:

<bpmn:scriptTask id="ProcessPayment" name="Process Payment">
<bpmn:script>
if (context.accountBalance < context.paymentAmount) {
throw new BpmnError('INSUFFICIENT_FUNDS',
'Account balance $' + context.accountBalance +
' is less than payment amount $' + context.paymentAmount);
}

// Process payment...
</bpmn:script>
</bpmn:scriptTask>

<!-- Interrupting boundary - cancels task -->
<bpmn:boundaryEvent id="InsufficientFunds"
attachedToRef="ProcessPayment"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientFundsError" />
</bpmn:boundaryEvent>

<!-- Error handler -->
<bpmn:scriptTask id="NotifyInsufficientFunds" name="Notify Customer">
<bpmn:script>
var error = context._lastError;

BankLingo.ExecuteCommand('SendEmail', {
to: context.customerEmail,
subject: 'Payment Failed - Insufficient Funds',
body: error.errorMessage
});

context.paymentStatus = 'FAILED';
context.failureReason = error.errorMessage;
</bpmn:script>
</bpmn:scriptTask>

<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />

Characteristics:

  • ✅ Task is canceled immediately
  • ✅ Process follows error path
  • ✅ Original task output is lost
  • ✅ Use for: Unrecoverable errors, critical failures

Non-Interrupting Boundary Events​

Non-interrupting boundary events handle errors without canceling the task:

<bpmn:userTask id="SubmitApplication" name="Submit Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="application-form"/>

<custom:property name="ServerScript"><![CDATA[
var errors = [];

if (!formData.email || !formData.email.includes('@')) {
errors.push('Valid email is required');
}

if (!formData.phoneNumber || formData.phoneNumber.length < 10) {
errors.push('Valid phone number is required');
}

if (errors.length > 0) {
throw new BpmnError('VALIDATION_ERROR', errors.join('; '));
}

// Save data if valid
context.email = formData.email;
context.phoneNumber = formData.phoneNumber;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

<!-- Non-interrupting boundary - task continues -->
<bpmn:boundaryEvent id="ValidationError"
attachedToRef="SubmitApplication"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>

<!-- Send validation errors to UI (task still active) -->
<bpmn:scriptTask id="NotifyValidationError" name="Notify Validation Error">
<bpmn:script>
var error = context._lastError;

// Send errors back to form UI
BankLingo.ExecuteCommand('SendFormErrors', {
taskId: 'SubmitApplication',
errors: error.errorMessage
});

logger.info('Form validation errors sent to UI');
</bpmn:script>
</bpmn:scriptTask>

<!-- User can correct and resubmit -->

<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />

Characteristics:

  • ✅ Task continues running
  • ✅ Error handled in parallel
  • ✅ Multiple error handlers can run
  • ✅ Use for: Validation errors, warnings, logging

Error Matching​

Match by Error Code​

Catch specific error codes:

<bpmn:scriptTask id="ValidateCredit" name="Validate Credit">
<bpmn:script>
if (context.creditScore < 600) {
throw new BpmnError('CREDIT_SCORE_TOO_LOW',
'Credit score ' + context.creditScore + ' is below minimum 600');
}

if (context.creditScore === null) {
throw new BpmnError('CREDIT_DATA_UNAVAILABLE',
'Credit bureau data unavailable');
}
</bpmn:script>
</bpmn:scriptTask>

<!-- Catch low credit score -->
<bpmn:boundaryEvent id="LowCreditScore"
attachedToRef="ValidateCredit"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="LowCreditError" />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="HandleLowCredit" name="Handle Low Credit">
<bpmn:script>
context.decisionReason = 'Credit score too low';
context.requiresManualReview = true;
</bpmn:script>
</bpmn:scriptTask>

<!-- Catch unavailable credit data -->
<bpmn:boundaryEvent id="CreditDataUnavailable"
attachedToRef="ValidateCredit"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="CreditDataError" />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="HandleCreditDataError" name="Retry Credit Check">
<bpmn:script>
context.creditCheckRetries = (context.creditCheckRetries || 0) + 1;

if (context.creditCheckRetries < 3) {
// Loop back to retry
context.retryCreditCheck = true;
} else {
// Give up after 3 retries
throw new BpmnError('CREDIT_CHECK_FAILED',
'Credit check failed after 3 attempts');
}
</bpmn:script>
</bpmn:scriptTask>

<bpmn:error id="LowCreditError" errorCode="CREDIT_SCORE_TOO_LOW" />
<bpmn:error id="CreditDataError" errorCode="CREDIT_DATA_UNAVAILABLE" />

Catch All Errors​

Catch any error with a generic boundary:

<bpmn:serviceTask id="CallExternalAPI" name="Call External API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CallPartnerAPICommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Catch all errors (no errorRef specified) -->
<bpmn:boundaryEvent id="AnyError"
attachedToRef="CallExternalAPI"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>

<!-- Generic error handler -->
<bpmn:scriptTask id="HandleAnyError" name="Handle Any Error">
<bpmn:script>
var error = context._lastError;

logger.error('API call failed: ' + error.errorCode + ' - ' + error.errorMessage);

// Determine action based on error code
switch (error.errorCode) {
case 'GATEWAY_TIMEOUT':
case 'GATEWAY_ERROR':
context.errorAction = 'RETRY';
break;
case 'AUTHENTICATION_ERROR':
context.errorAction = 'REFRESH_TOKEN';
break;
default:
context.errorAction = 'ESCALATE';
}
</bpmn:script>
</bpmn:scriptTask>

Multiple Boundary Events​

Attach multiple boundaries for different error scenarios:

<bpmn:serviceTask id="ProcessTransaction" name="Process Transaction">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="ProcessTransactionCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Boundary 1: Insufficient funds -->
<bpmn:boundaryEvent id="InsufficientFunds"
attachedToRef="ProcessTransaction"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientFundsError" />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="NotifyInsufficientFunds" name="Notify Customer">
<!-- Handler for insufficient funds -->
</bpmn:scriptTask>

<!-- Boundary 2: Transaction declined -->
<bpmn:boundaryEvent id="TransactionDeclined"
attachedToRef="ProcessTransaction"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="DeclinedError" />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="HandleDeclined" name="Handle Declined">
<!-- Handler for declined transaction -->
</bpmn:scriptTask>

<!-- Boundary 3: Gateway timeout (retryable) -->
<bpmn:boundaryEvent id="GatewayTimeout"
attachedToRef="ProcessTransaction"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="TimeoutError" />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="RetryTransaction" name="Retry Transaction">
<!-- Non-interrupting retry handler -->
</bpmn:scriptTask>

<!-- Boundary 4: Any other error -->
<bpmn:boundaryEvent id="OtherError"
attachedToRef="ProcessTransaction"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>

<bpmn:scriptTask id="HandleOtherError" name="Handle Other Error">
<!-- Generic error handler -->
</bpmn:scriptTask>

<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />
<bpmn:error id="DeclinedError" errorCode="TRANSACTION_DECLINED" />
<bpmn:error id="TimeoutError" errorCode="GATEWAY_TIMEOUT" />

Matching Priority:

  1. Specific error code match (highest priority)
  2. Generic catch-all boundary (lowest priority)
  3. First matching boundary wins (if multiple match)

Common Patterns​

Pattern 1: Retry with Boundary Events​

<bpmn:serviceTask id="CallAPI" name="Call External API">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="CallAPICommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Non-interrupting for retryable errors -->
<bpmn:boundaryEvent id="RetryableError"
attachedToRef="CallAPI"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="RetryableError" />
</bpmn:boundaryEvent>

<!-- Interrupting for permanent errors -->
<bpmn:boundaryEvent id="PermanentError"
attachedToRef="CallAPI"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="PermanentError" />
</bpmn:boundaryEvent>

<!-- Retry logic -->
<bpmn:scriptTask id="CalculateRetry" name="Calculate Retry">
<bpmn:script>
var retryCount = (context.apiRetryCount || 0) + 1;
var maxRetries = 3;

if (retryCount > maxRetries) {
// Convert to permanent error
throw new BpmnError('API_RETRY_EXHAUSTED',
'Failed after ' + maxRetries + ' attempts');
}

context.apiRetryCount = retryCount;
// Exponential backoff: 2^n * 1000ms
context.retryDelayMs = Math.pow(2, retryCount) * 1000;

logger.info('Retry ' + retryCount + ' after ' + context.retryDelayMs + 'ms');
</bpmn:script>
</bpmn:scriptTask>

<!-- Wait before retry -->
<bpmn:intermediateCatchEvent id="WaitBeforeRetry" name="Wait">
<bpmn:timerEventDefinition>
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">
PT${context.retryDelayMs}MS
</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>

<!-- Loop back -->
<bpmn:sequenceFlow sourceRef="WaitBeforeRetry" targetRef="CallAPI" />

<!-- Handle permanent failure -->
<bpmn:scriptTask id="HandlePermanentError" name="Handle Permanent Error">
<bpmn:script>
var error = context._lastError;
logger.error('API permanently failed: ' + error.errorMessage);

context.apiCallFailed = true;
context.useAlternativeMethod = true;
</bpmn:script>
</bpmn:scriptTask>

<bpmn:error id="RetryableError" errorCode="GATEWAY_TIMEOUT" />
<bpmn:error id="PermanentError" errorCode="API_RETRY_EXHAUSTED" />

Pattern 2: Escalation on Error​

<bpmn:userTask id="TeamLeadApproval" name="Team Lead Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ServerScript"><![CDATA[
// Check approval authority
if (context.amount > 50000) {
throw new BpmnError('INSUFFICIENT_AUTHORITY',
'Amount exceeds team lead approval limit of $50,000');
}

context.approvedBy = formData.userId;
context.approvalComments = formData.comments;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

<!-- Escalate to manager -->
<bpmn:boundaryEvent id="EscalateToManager"
attachedToRef="TeamLeadApproval"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="InsufficientAuthorityError" />
</bpmn:boundaryEvent>

<!-- Manager approval task -->
<bpmn:userTask id="ManagerApproval" name="Manager Approval (Escalated)">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleTeams" value="Managers"/>
<custom:property name="Description" value="ESCALATED: Amount exceeds team lead authority"/>
<custom:property name="Priority" value="HIGH"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

<bpmn:error id="InsufficientAuthorityError" errorCode="INSUFFICIENT_AUTHORITY" />

Pattern 3: Validation Loop​

<bpmn:userTask id="CustomerForm" name="Customer Form">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="customer-form"/>

<custom:property name="ServerScript"><![CDATA[
var errors = [];

// Validate email
if (!formData.email || !formData.email.includes('@')) {
errors.push('Valid email is required');
}

// Validate phone
if (!formData.phone || formData.phone.length < 10) {
errors.push('Valid 10-digit phone number is required');
}

// Validate age
if (!formData.age || formData.age < 18) {
errors.push('Must be 18 years or older');
}

if (errors.length > 0) {
throw new BpmnError('VALIDATION_ERROR', errors.join('; '));
}

// Save valid data
context.customerEmail = formData.email;
context.customerPhone = formData.phone;
context.customerAge = formData.age;
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>

<!-- Non-interrupting validation error -->
<bpmn:boundaryEvent id="ValidationError"
attachedToRef="CustomerForm"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>

<!-- Send errors to UI -->
<bpmn:scriptTask id="SendValidationErrors" name="Send Validation Errors">
<bpmn:script>
var error = context._lastError;

// Send to UI for display
BankLingo.ExecuteCommand('SendFormErrors', {
taskId: 'CustomerForm',
errors: error.errorMessage
});

// Track validation attempts
context.validationAttempts = (context.validationAttempts || 0) + 1;

if (context.validationAttempts > 5) {
logger.warn('Customer exceeded 5 validation attempts');
// Could send help email or close form
}
</bpmn:script>
</bpmn:scriptTask>

<!-- User corrects and resubmits (task still active) -->

<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />

Pattern 4: Fallback on Error​

<bpmn:serviceTask id="PrimaryService" name="Call Primary Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="PrimaryServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Catch primary service error -->
<bpmn:boundaryEvent id="PrimaryServiceError"
attachedToRef="PrimaryService"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>

<!-- Try fallback service -->
<bpmn:serviceTask id="FallbackService" name="Call Fallback Service">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="FallbackServiceCommand"/>
<custom:property name="ResultVariable" value="serviceResult"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>

<!-- Catch fallback error -->
<bpmn:boundaryEvent id="FallbackServiceError"
attachedToRef="FallbackService"
cancelActivity="true">
<bpmn:errorEventDefinition />
</bpmn:boundaryEvent>

<!-- Use cached data as last resort -->
<bpmn:scriptTask id="UseCachedData" name="Use Cached Data">
<bpmn:script>
logger.warn('Both primary and fallback services failed. Using cached data.');

context.serviceResult = context.cachedServiceData || {};
context.dataSource = 'CACHE';
context.dataFreshness = 'STALE';
context.cacheAge = Date.now() - new Date(context.cacheTimestamp).getTime();

if (!context.cachedServiceData) {
throw new BpmnError('NO_DATA_AVAILABLE',
'All data sources failed and no cache available');
}
</bpmn:script>
</bpmn:scriptTask>

<!-- Final error if even cache fails -->
<bpmn:boundaryEvent id="NoDataAvailable"
attachedToRef="UseCachedData"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="NoDataError" />
</bpmn:boundaryEvent>

<bpmn:error id="NoDataError" errorCode="NO_DATA_AVAILABLE" />

Best Practices​

✅ Do This​

<!-- ✅ Use interrupting for unrecoverable errors -->
<bpmn:boundaryEvent cancelActivity="true">
<bpmn:errorEventDefinition errorRef="FatalError"/>
</bpmn:boundaryEvent>

<!-- ✅ Use non-interrupting for validation errors -->
<bpmn:boundaryEvent cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError"/>
</bpmn:boundaryEvent>

<!-- ✅ Catch specific errors -->
<bpmn:boundaryEvent>
<bpmn:errorEventDefinition errorRef="SpecificError"/>
</bpmn:boundaryEvent>

<!-- ✅ Add generic catch-all as fallback -->
<bpmn:boundaryEvent>
<bpmn:errorEventDefinition/> <!-- Catches all errors -->
</bpmn:boundaryEvent>

<!-- ✅ Log errors in handlers -->
<bpmn:script>
var error = context._lastError;
logger.error('Error caught: ' + error.errorCode);
</bpmn:script>

❌ Don't Do This​

<!-- ❌ Missing boundary events -->
<bpmn:scriptTask id="RiskyTask">
<!-- No error boundaries - errors crash process -->
</bpmn:scriptTask>

<!-- ❌ Wrong cancelActivity for use case -->
<bpmn:boundaryEvent cancelActivity="true"> <!-- Should be false -->
<bpmn:errorEventDefinition errorRef="ValidationError"/>
</bpmn:boundaryEvent>

<!-- ❌ Too many catch-alls (makes debugging hard) -->
<bpmn:boundaryEvent>
<bpmn:errorEventDefinition/> <!-- Which errors? -->
</bpmn:boundaryEvent>

<!-- ❌ No error handling in boundary task -->
<bpmn:scriptTask id="ErrorHandler">
<bpmn:script>
// No access to error context, no logging
return { handled: true };
</bpmn:script>
</bpmn:scriptTask>

Error Definition​

Define errors in BPMN XML:

<!-- Error definitions -->
<bpmn:error id="ValidationError" errorCode="VALIDATION_ERROR" />
<bpmn:error id="InsufficientFundsError" errorCode="INSUFFICIENT_FUNDS" />
<bpmn:error id="TimeoutError" errorCode="GATEWAY_TIMEOUT" />
<bpmn:error id="AuthenticationError" errorCode="AUTHENTICATION_ERROR" />

Properties:

  • id: Unique identifier for BPMN references
  • errorCode: The code thrown by BpmnError (must match)

Features Used:

  • Phase 5: Error Handling

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