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