UserTask - Human Interactions
UserTask enables human interaction in automated processes. When execution reaches a UserTask, the process pauses and waits for a user to complete the task by performing an action (approve, reject, submit, etc.).
Properties
Required Properties
FormKey: The form identifier for UI renderingTaskType: Must be "UserTask"Name: Display name of the task
Optional Properties
Script(PreScript): JavaScript code executed before task pauses, preparesformContextfor UIServerScript: JavaScript code executed after user submits form, validatesformDataClientScript: JavaScript code for client-side UI logic (runs in browser)ResultVariable: Variable name to store ServerScript resultUserActions: Array of available actions (e.g., ["Approve", "Reject"])EntityState: State to set when task becomes activeResponsibleTeams: Teams assigned to this taskResponsibleUsers: Specific users assigned to this taskDescription: Task descriptionInputMapping: Map process variables to task inputsOutputMapping: Map task outputs to process variables
UserTask supports three script types that execute at different stages:
- PreScript (
Scriptproperty): Runs when task is reached, before pause → preparesformContext - ClientScript (
ClientScriptproperty): Runs in browser for UI logic - ServerScript (
ServerScriptproperty): Runs after signal/resume, validatesformData
Script Execution Lifecycle
Complete UserTask Flow
1. PreScript (Script Property)
Timing: Executes before process pauses
Purpose: Prepare data for the frontend form
Context: Has access to all process variables
Result: Stored in formContext and sent to UI
<bpmn:userTask id="Task_ReviewLoan" name="Review Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="loan-review-form" />
<!-- PreScript: Prepare data for form -->
<custom:property name="Script" value="
// Fetch additional data needed for the form
const customer = doCmd('GetCustomerProfile', { customerId });
const creditReport = doCmd('GetCreditReport', { customerId });
const accountHistory = doCmd('GetAccountHistory', { customerId });
// Calculate recommendation
let recommendation = 'Approve';
if (creditReport.score < 650) recommendation = 'Reject';
else if (loanAmount > customer.monthlyIncome * 4) recommendation = 'Review';
// Return formContext object that will be sent to UI
return {
customer: {
name: customer.fullName,
email: customer.email,
monthlyIncome: customer.monthlyIncome
},
credit: {
score: creditReport.score,
rating: creditReport.rating
},
application: {
amount: loanAmount,
term: termMonths,
purpose: loanPurpose
},
recommendation: recommendation,
accountAge: accountHistory.ageMonths,
averageBalance: accountHistory.averageBalance
};
" />
<custom:property name="UserActions" value="Approve,Reject,RequestMoreInfo" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
Engine Behavior:
When the engine reaches this UserTask:
- Execute PreScript - Runs the Script property with current process variables
- Store Form Context - Result is stored as
formContextin the waiting task - Pause Process - Process status changes to "Waiting", execution stops
- Send to Frontend - Waiting task (with formContext) is returned to the client
2. ClientScript (Browser)
Timing: Executes in browser when form loads
Purpose: Dynamic UI behavior (show/hide fields, validation)
Context: Has access to formContext
Result: UI changes only (does not affect process)
<custom:property name="ClientScript" value="
// Show/hide fields based on form data
if (formContext.application.amount > 100000) {
showField('managerApprovalJustification');
requireField('managerApprovalJustification');
}
if (formContext.credit.score < 650) {
showWarning('Low credit score - Additional documentation required');
showField('additionalDocuments');
}
if (formContext.recommendation === 'Reject') {
highlightField('recommendation', 'red');
}
" />
3. ServerScript (Post-Submit Validation)
Timing: Executes after user submits form and before process continues
Purpose: Server-side validation of user input
Context: Has access to all process variables + formData
Result: Can block process if validation fails
<bpmn:userTask id="Task_ApprovalDecision" name="Make Approval Decision">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form" />
<custom:property name="UserActions" value="Approve,Reject" />
<!-- ServerScript: Validate submission -->
<custom:property name="ServerScript" value="
// formData is now merged into context
const action = userAction; // From form signal
const comments = approverComments; // From form signal
// Validation rules
if (action === 'Approve' && loanAmount > 500000 && !comments) {
throw new Error('Approval comments are required for loans over $500,000');
}
if (action === 'Reject' && !comments) {
throw new Error('Rejection reason is required');
}
// Additional business logic validation
if (action === 'Approve' && creditScore < 600) {
throw new Error('Cannot approve application with credit score below 600');
}
// Log approval decision
const auditLog = {
decision: action,
approver: currentUser,
timestamp: new Date().toISOString(),
loanAmount: loanAmount,
comments: comments
};
// Store audit log
doCmd('LogApprovalDecision', auditLog);
return {
validated: true,
validatedBy: currentUser,
validationTime: new Date().toISOString()
};
" />
<!-- Store validation result -->
<custom:property name="ResultVariable" value="validationResult" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
Engine Behavior:
When the user submits the form (signal/resume):
- Merge Form Data - All submitted form fields are merged into the process context
- Execute ServerScript - Runs with full context including submitted formData
- Store Result - ServerScript result stored in ResultVariable (or lastScriptResult if not specified)
- Continue or Block - If validation passes, process continues. If validation fails (throws error), process goes to Error state
BPMN XML Example
Basic UserTask
<bpmn:userTask id="Task_ReviewApplication" name="Review Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="loan-review-form" />
<custom:property name="UserActions" value="Approve,Reject" />
<custom:property name="EntityState" value="PendingReview" />
<custom:property name="ResponsibleTeams" value="LoanOfficers" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_ToReview</bpmn:incoming>
<bpmn:outgoing>Flow_AfterReview</bpmn:outgoing>
</bpmn:userTask>
Advanced UserTask with Multiple Actions
<bpmn:userTask id="Task_ComplexApproval" name="Multi-Level Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="complex-approval-form" />
<custom:property name="UserActions" value="Approve,Reject,RequestMoreInfo,EscalateToManager" />
<custom:property name="EntityState" value="PendingComplexApproval" />
<custom:property name="Description" value="Review the request and take appropriate action" />
<!-- Multiple teams can handle this -->
<custom:property name="ResponsibleTeams" value="LoanOfficers,SeniorOfficers,Managers" />
<!-- Specific users can also be assigned -->
<custom:property name="ResponsibleUsers" value="john.doe@bank.com,jane.smith@bank.com" />
<!-- Client-side script for dynamic form behavior -->
<custom:property name="ClientScript" value="
if (amount > 100000) {
showField('managerApprovalRequired');
require('managerComments');
}
if (riskScore > 75) {
showWarning('High risk application');
}
" />
<!-- Input mapping: Read from process variables -->
<custom:property name="InputMapping" value="{
"applicationAmount": "loanAmount",
"customerInfo": "customer",
"riskAssessment": "riskScore"
}" />
<!-- Output mapping: Write to process variables -->
<custom:property name="OutputMapping" value="{
"approvalDecision": "decision",
"approverComments": "comments",
"approvalTimestamp": "timestamp"
}" />
</custom:properties>
</bpmn:extensionElements>
<bpmn:incoming>Flow_In</bpmn:incoming>
<bpmn:outgoing>Flow_Out</bpmn:outgoing>
</bpmn:userTask>
API Usage
1. Query Active User Tasks
Find tasks assigned to a user or team:
GET /api/user-tasks/active?assignedToTeam=LoanOfficers
Authorization: Bearer YOUR_API_TOKEN
Response:
{
"success": true,
"data": [
{
"taskId": "Task_ReviewApplication",
"taskName": "Review Loan Application",
"instanceGuid": "660e8400-e29b-41d4-a716-446655440001",
"processKey": "LoanApproval_v1",
"businessKey": "LOAN-2025-001",
"formKey": "loan-review-form",
"userActions": ["Approve", "Reject"],
"responsibleTeams": ["LoanOfficers"],
"entityState": "PendingReview",
"createdAt": "2025-12-18T10:00:00Z",
"variables": {
"loanAmount": 50000,
"customerName": "John Doe",
"applicationDate": "2025-12-18"
}
}
]
}
2. Get User Task Details
Get detailed information about a specific task:
GET /api/user-tasks/{instanceGuid}/{taskId}
Authorization: Bearer YOUR_API_TOKEN
Response:
{
"success": true,
"data": {
"taskId": "Task_ReviewApplication",
"taskName": "Review Loan Application",
"formKey": "loan-review-form",
"userActions": ["Approve", "Reject"],
"description": "Review the loan application and make a decision",
"entityState": "PendingReview",
"responsibleTeams": ["LoanOfficers"],
"responsibleUsers": [],
"inputData": {
"applicationAmount": 50000,
"customerInfo": {
"name": "John Doe",
"email": "john@example.com"
},
"riskAssessment": 65
},
"metadata": {
"createdAt": "2025-12-18T10:00:00Z",
"dueDate": "2025-12-20T17:00:00Z",
"priority": "Normal"
}
}
}
3. Complete User Task
Complete the task with an action and optional variables:
POST /api/user-tasks/complete
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
{
"instanceGuid": "660e8400-e29b-41d4-a716-446655440001",
"taskId": "Task_ReviewApplication",
"userAction": "Approve",
"variables": {
"decision": "Approved",
"comments": "Application meets all criteria. Approved for full amount.",
"approverName": "Jane Smith",
"approvalDate": "2025-12-18T14:30:00Z",
"conditions": ["Credit check passed", "Income verified"]
}
}
Response:
{
"success": true,
"data": {
"instanceGuid": "660e8400-e29b-41d4-a716-446655440001",
"taskCompleted": true,
"nextTaskId": "Task_ProcessApproval",
"status": "Running",
"completedAt": "2025-12-18T14:30:05Z"
}
}
4. Claim/Assign Task
Assign a task to a specific user:
POST /api/user-tasks/claim
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
{
"instanceGuid": "660e8400-e29b-41d4-a716-446655440001",
"taskId": "Task_ReviewApplication",
"userId": "jane.smith@bank.com"
}
5. Reassign Task
Transfer task to another user:
POST /api/user-tasks/reassign
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
{
"instanceGuid": "660e8400-e29b-41d4-a716-446655440001",
"taskId": "Task_ReviewApplication",
"fromUserId": "john.doe@bank.com",
"toUserId": "jane.smith@bank.com",
"reason": "John is on vacation"
}
Use Cases
1. Simple Approval
Scenario: Manager approves or rejects a request
<bpmn:userTask id="Task_ManagerApproval" name="Manager Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="simple-approval" />
<custom:property name="UserActions" value="Approve,Reject" />
<custom:property name="ResponsibleTeams" value="Managers" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
2. Document Verification
Scenario: Staff verifies uploaded documents
<bpmn:userTask id="Task_VerifyDocuments" name="Verify Customer Documents">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="document-verification" />
<custom:property name="UserActions" value="VerifyAll,RejectSome,RequestRescan" />
<custom:property name="EntityState" value="DocumentVerification" />
<custom:property name="ResponsibleTeams" value="ComplianceTeam,BackOffice" />
<custom:property name="InputMapping" value="{
"documents": "uploadedDocuments",
"customerName": "customer.name"
}" />
<custom:property name="OutputMapping" value="{
"verificationStatus": "status",
"verifiedBy": "verifier",
"verifiedDocuments": "verified"
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
3. Multi-Step Approval Chain
Scenario: Sequential approvals by different levels
<!-- Step 1: Loan Officer -->
<bpmn:userTask id="Task_OfficerReview" name="Loan Officer Review">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="officer-review" />
<custom:property name="UserActions" value="Recommend,Reject" />
<custom:property name="ResponsibleTeams" value="LoanOfficers" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<bpmn:sequenceFlow sourceRef="Task_OfficerReview" targetRef="Gateway_OfficerDecision" />
<!-- Gateway checks if recommended -->
<bpmn:exclusiveGateway id="Gateway_OfficerDecision">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Condition" value="return userAction === 'Recommend' ? 'Flow_ToManager' : 'Flow_Rejected';" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:exclusiveGateway>
<!-- Step 2: Manager Approval -->
<bpmn:sequenceFlow id="Flow_ToManager" sourceRef="Gateway_OfficerDecision" targetRef="Task_ManagerApproval" />
<bpmn:userTask id="Task_ManagerApproval" name="Manager Final Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="manager-approval" />
<custom:property name="UserActions" value="Approve,Reject" />
<custom:property name="ResponsibleTeams" value="Managers" />
<custom:property name="InputMapping" value="{
"officerRecommendation": "officerComments"
}" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
4. Conditional User Task
Scenario: Task only appears if certain conditions are met
<bpmn:exclusiveGateway id="Gateway_CheckAmount" name="High Value?">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="Condition" value="return amount > 100000 ? 'Flow_HighValue' : 'Flow_Standard';" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:exclusiveGateway>
<!-- High value requires manual review -->
<bpmn:sequenceFlow id="Flow_HighValue" sourceRef="Gateway_CheckAmount" targetRef="Task_SeniorOfficerReview" />
<bpmn:userTask id="Task_SeniorOfficerReview" name="Senior Officer Review (High Value)">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="senior-review" />
<custom:property name="UserActions" value="Approve,Reject,EscalateToBoard" />
<custom:property name="ResponsibleTeams" value="SeniorOfficers" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Standard amount auto-approved -->
<bpmn:sequenceFlow id="Flow_Standard" sourceRef="Gateway_CheckAmount" targetRef="Task_AutoApprove" />
Best Practices
1. Clear Action Names
Use descriptive, business-friendly action names:
<!-- Good -->
<custom:property name="UserActions" value="Approve,Reject,RequestMoreInfo" />
<!-- Avoid -->
<custom:property name="UserActions" value="Yes,No,Maybe" />
2. Provide Meaningful Descriptions
<custom:property name="Description" value="Review the customer's credit history, verify income documents, and assess risk before making approval decision." />
3. Use Input/Output Mapping
Keep process variables clean by mapping only necessary data:
<custom:property name="InputMapping" value="{
"displayAmount": "loanAmount",
"customerSummary": "customer.summary"
}" />
<custom:property name="OutputMapping" value="{
"approvalResult": "result",
"approverNotes": "notes"
}" />
4. Set Appropriate Entity States
Use entity states for tracking and reporting:
<custom:property name="EntityState" value="PendingManagerApproval" />
5. Assign to Teams, Not Individuals
Prefer team assignment for flexibility:
<!-- Good - Any team member can handle -->
<custom:property name="ResponsibleTeams" value="LoanOfficers" />
<!-- Avoid - Creates bottleneck -->
<custom:property name="ResponsibleUsers" value="john.doe@bank.com" />
WorkflowData, Script, and ClientScript: Secure Context Preparation
How UserTask Context is Prepared
When a UserTask is reached, the process engine prepares a context object (WorkflowData) that is sent to the frontend for form rendering. By default, only variables explicitly returned by the UserTask's Script property (server-side) are included in WorkflowData.
1. Script (Server-Side Context Preparation)
- The
Scriptproperty allows you to write C# code that runs on the server to prepare the context for the form. - Only variables returned from this script are exposed to the frontend as
WorkflowData. - This ensures sensitive process variables are not leaked to the UI.
- If no Script is provided, the engine falls back to
FormVariables(if defined), or provides an empty context.
Example:
<custom:property name="Script" value="return new { applicationAmount = loanAmount, customerInfo = customer, riskAssessment = riskScore };" />
2. ClientScript (Client-Side UI Logic)
- The
ClientScriptproperty contains JavaScript code that runs in the browser. - It can use the variables in
WorkflowDatato control UI behavior (e.g., show/hide fields, validate input). - It cannot access any process variables not explicitly included in
WorkflowData.
Example:
<custom:property name="ClientScript" value="if (riskAssessment > 75) { showWarning('High risk application'); }" />
3. Security Best Practices
- Never expose all process variables to the frontend.
- Always use
Scriptto whitelist only the variables/forms needed for the user. - This prevents accidental data leaks and enforces least-privilege access.
4. Example: Secure UserTask Context
<bpmn:userTask id="Task_ReviewApplication" name="Review Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="loan-review-form" />
<custom:property name="UserActions" value="Approve,Reject" />
<custom:property name="Script" value="return new { applicationAmount = loanAmount, customerInfo = customer, riskAssessment = riskScore };" />
<custom:property name="ClientScript" value="if (riskAssessment > 75) { showWarning('High risk application'); }" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
5. How the Engine Prepares WorkflowData
- The engine uses the following priority:
- If
Scriptis present, it executes it and returns only those variables. - If
FormVariablesis present, it returns only those variables. - Otherwise, returns an empty object.
- If
- This is implemented in the
PrepareFormContextmethod in the backend.
6. ClientScript Example (UI Logic)
<custom:property name="ClientScript" value="if (applicationAmount > 100000) { showField('managerApprovalRequired'); require('managerComments'); }" />
Integration with BankLingo Forms
UserTasks typically render forms in the BankLingo UI:
// Form rendering (client-side)
const formConfig = {
formKey: 'loan-review-form',
fields: [
{
name: 'applicationAmount',
type: 'currency',
label: 'Loan Amount',
readonly: true
},
{
name: 'comments',
type: 'textarea',
label: 'Reviewer Comments',
required: true
},
{
name: 'decision',
type: 'select',
label: 'Decision',
options: ['Approve', 'Reject'],
required: true
}
],
actions: ['Approve', 'Reject']
};
Async Boundaries (Phase 2)
Background User Task Creation
Use async boundaries to create user tasks in the background without blocking the API response:
<bpmn:userTask id="ManagerApproval"
name="Manager Approval"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="manager-approval-form"/>
<custom:property name="ResponsibleTeams" value="LoanManagers"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
Benefits:
- ✅ API returns immediately to client
- ✅ User task created in background
- ✅ Improved mobile app responsiveness
- ✅ Better user experience
Example Flow:
User submits loan application
→ API returns 200 OK immediately
→ Background: Process continues
→ Background: User task "Manager Approval" created
→ Manager sees task in their dashboard
Mobile App Pattern
<bpmn:process id="MobileLoanApplication">
<!-- Initial submission -->
<bpmn:startEvent id="Start"/>
<!-- Validate immediately (synchronous) -->
<bpmn:scriptTask id="ValidateInput" name="Validate Input">
<bpmn:script>
if (!context.loanAmount || context.loanAmount <= 0) {
throw new BpmnError('VALIDATION_ERROR', 'Invalid loan amount');
}
return { validated: true };
</bpmn:script>
</bpmn:scriptTask>
<!-- Async boundary - API returns here -->
<bpmn:userTask id="DocumentUpload"
name="Upload Documents"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="document-upload-form"/>
<custom:property name="Description" value="Please upload required documents"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Customer completes document upload -->
<!-- Another async boundary -->
<bpmn:userTask id="ManagerReview"
name="Manager Review"
camunda:asyncBefore="true">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="manager-review-form"/>
<custom:property name="ResponsibleTeams" value="LoanManagers"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
</bpmn:process>
Timeout Handling (Phase 4)
User Task Timeouts with Timer Boundaries
Handle cases where users don't complete tasks within expected time:
<bpmn:userTask id="CustomerDocumentSubmission"
name="Submit Documents">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="document-submission-form"/>
<custom:property name="Description" value="Please submit required documents within 7 days"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Timeout after 7 days -->
<bpmn:boundaryEvent id="DocumentTimeout"
attachedToRef="CustomerDocumentSubmission"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P7D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Timeout handler -->
<bpmn:scriptTask id="HandleDocumentTimeout" name="Handle Timeout">
<bpmn:script>
logger.warn('Customer failed to submit documents within 7 days');
// Send reminder or close application
BankLingo.ExecuteCommand('SendEmail', {
to: context.customerEmail,
subject: 'Application Expired',
body: 'Your loan application has been closed due to missing documents.'
});
context.applicationStatus = 'EXPIRED';
context.expiryReason = 'DOCUMENT_SUBMISSION_TIMEOUT';
return { handled: true };
</bpmn:script>
</bpmn:scriptTask>
Non-Interrupting Timeout (Reminder)
Send reminder but keep task active:
<bpmn:userTask id="ManagerApproval" name="Manager Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleTeams" value="Managers"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Send reminder after 24 hours (non-interrupting) -->
<bpmn:boundaryEvent id="ReminderTimer"
attachedToRef="ManagerApproval"
cancelActivity="false">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P1D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Send reminder (task continues) -->
<bpmn:sendTask id="SendReminder" name="Send Reminder">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="messageType" value="email"/>
<custom:property name="to" value="context.managerEmail"/>
<custom:property name="subject" value="Reminder: Approval Pending"/>
<custom:property name="body" value="Loan application {{context.loanId}} awaiting approval for 24 hours"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:sendTask>
Escalation Pattern
<bpmn:userTask id="TeamLeadApproval" name="Team Lead Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleUsers" value="context.teamLeadEmail"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Reminder after 12 hours (non-interrupting) -->
<bpmn:boundaryEvent id="Reminder12h"
attachedToRef="TeamLeadApproval"
cancelActivity="false">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT12H</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<bpmn:sendTask id="SendReminderToTeamLead" name="Remind Team Lead"/>
<!-- Escalate after 24 hours (interrupting) -->
<bpmn:boundaryEvent id="Escalate24h"
attachedToRef="TeamLeadApproval"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P1D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Escalate to manager -->
<bpmn:userTask id="ManagerApproval" name="Manager Approval (Escalated)">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleUsers" value="context.managerEmail"/>
<custom:property name="Description" value="ESCALATED: Team lead approval timed out"/>
<custom:property name="Priority" value="HIGH"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
Timeout with Multiple Reminders
<bpmn:userTask id="DocumentReview" name="Review Documents">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="review-form"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- First reminder after 1 day -->
<bpmn:boundaryEvent id="Reminder1Day"
attachedToRef="DocumentReview"
cancelActivity="false">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P1D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Second reminder after 3 days -->
<bpmn:boundaryEvent id="Reminder3Days"
attachedToRef="DocumentReview"
cancelActivity="false">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P3D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Final timeout after 7 days -->
<bpmn:boundaryEvent id="Timeout7Days"
attachedToRef="DocumentReview"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P7D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
Error Handling (Phase 5)
Validation Errors in ServerScript
Throw BpmnError from ServerScript to reject invalid form submissions:
<bpmn:userTask id="LoanApplicationForm" name="Loan Application">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="loan-application"/>
<!-- ServerScript validates user input -->
<custom:property name="ServerScript"><![CDATA[
// formData contains user's submitted form data
var errors = [];
// Validate loan amount
if (!formData.loanAmount || formData.loanAmount <= 0) {
errors.push('Loan amount must be greater than zero');
}
if (formData.loanAmount > 1000000) {
errors.push('Loan amount cannot exceed $1,000,000');
}
// Validate credit score
if (!formData.creditScore || formData.creditScore < 300 || formData.creditScore > 850) {
errors.push('Credit score must be between 300 and 850');
}
// Validate email
if (!formData.email || !formData.email.includes('@')) {
errors.push('Valid email address is required');
}
// Validate phone
if (!formData.phone || formData.phone.length < 10) {
errors.push('Valid 10-digit phone number is required');
}
// If any errors, throw BpmnError
if (errors.length > 0) {
throw new BpmnError('FORM_VALIDATION_ERROR',
'Form validation failed: ' + errors.join('; '));
}
// Validation passed - save to context
context.loanAmount = formData.loanAmount;
context.creditScore = formData.creditScore;
context.customerEmail = formData.email;
context.customerPhone = formData.phone;
return {
validated: true,
timestamp: new Date().toISOString()
};
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Catch validation errors -->
<bpmn:boundaryEvent id="FormValidationError"
attachedToRef="LoanApplicationForm"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="ValidationError" />
</bpmn:boundaryEvent>
<!-- Send validation errors back to user -->
<bpmn:scriptTask id="NotifyValidationError" name="Notify User">
<bpmn:script>
var error = context._lastError;
// Send error message to UI
BankLingo.ExecuteCommand('SendFormErrors', {
taskId: 'LoanApplicationForm',
errors: error.errorMessage
});
logger.info('Form validation failed: ' + error.errorMessage);
return { notified: true };
</bpmn:script>
</bpmn:scriptTask>
<!-- Loop back to let user correct the form -->
<bpmn:sequenceFlow sourceRef="NotifyValidationError" targetRef="LoanApplicationForm" />
<bpmn:error id="ValidationError" errorCode="FORM_VALIDATION_ERROR" />
Business Rule Validation
<bpmn:userTask id="ApprovalDecision" name="Approve or Reject">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="UserActions" value="Approve,Reject"/>
<custom:property name="ServerScript"><![CDATA[
// formData contains: { action: 'Approve', comments: '...' }
if (formData.action === 'Approve') {
// Check if approver has authority for this amount
var maxApprovalAmount = context.approverMaxAmount || 50000;
if (context.loanAmount > maxApprovalAmount) {
throw new BpmnError('INSUFFICIENT_AUTHORITY',
'Approver cannot approve amounts over $' + maxApprovalAmount);
}
// Check if all required documents uploaded
var requiredDocs = ['ID', 'PROOF_OF_INCOME', 'BANK_STATEMENT'];
var uploadedDocs = context.uploadedDocuments || [];
var missingDocs = requiredDocs.filter(d => !uploadedDocs.includes(d));
if (missingDocs.length > 0) {
throw new BpmnError('MISSING_DOCUMENTS',
'Cannot approve with missing documents: ' + missingDocs.join(', '));
}
}
// Save decision
context.approvalDecision = formData.action;
context.approvalComments = formData.comments;
context.approvedBy = formData.userId;
context.approvalDate = new Date().toISOString();
return {
decision: formData.action,
valid: true
};
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Catch business rule violations -->
<bpmn:boundaryEvent id="BusinessRuleError"
attachedToRef="ApprovalDecision"
cancelActivity="true">
<bpmn:errorEventDefinition errorRef="BusinessRuleError" />
</bpmn:boundaryEvent>
<!-- Escalate to higher authority -->
<bpmn:userTask id="SeniorManagerApproval" name="Senior Manager Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleTeams" value="SeniorManagers"/>
<custom:property name="Description" value="Escalated: Amount exceeds manager authority"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<bpmn:error id="BusinessRuleError" errorCode="BUSINESS_RULE_VIOLATION" />
Accessing Error Context
// In ServerScript or subsequent tasks
var error = context._lastError;
if (error) {
console.log('Last error code:', error.errorCode);
console.log('Last error message:', error.errorMessage);
console.log('Error occurred at:', error.timestamp);
console.log('Error occurred in task:', error.taskId);
}
// Access full error history
var errorHistory = context._errorHistory || [];
console.log('Total errors:', errorHistory.length);
Pattern: Approval with Dual Control
<bpmn:userTask id="FirstApprover" name="First Approver">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleTeams" value="Approvers"/>
<custom:property name="ServerScript"><![CDATA[
// Cannot approve own application
if (formData.userId === context.applicationCreatedBy) {
throw new BpmnError('CONFLICT_OF_INTEREST',
'Cannot approve your own application');
}
context.firstApprover = formData.userId;
context.firstApproverComments = formData.comments;
return { approved: true };
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<bpmn:userTask id="SecondApprover" name="Second Approver">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form"/>
<custom:property name="ResponsibleTeams" value="Approvers"/>
<custom:property name="ServerScript"><![CDATA[
// Cannot be the same person as first approver
if (formData.userId === context.firstApprover) {
throw new BpmnError('DUAL_CONTROL_VIOLATION',
'Second approver must be different from first approver');
}
// Cannot approve own application
if (formData.userId === context.applicationCreatedBy) {
throw new BpmnError('CONFLICT_OF_INTEREST',
'Cannot approve your own application');
}
context.secondApprover = formData.userId;
context.secondApproverComments = formData.comments;
return { approved: true };
]]></custom:property>
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Catch dual control violations -->
<bpmn:boundaryEvent id="DualControlError"
attachedToRef="SecondApprover"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="DualControlError" />
</bpmn:boundaryEvent>
<!-- Notify and loop back -->
<bpmn:scriptTask id="NotifyDualControlError" name="Notify Error">
<bpmn:script>
var error = context._lastError;
BankLingo.ExecuteCommand('SendFormErrors', {
taskId: 'SecondApprover',
errors: error.errorMessage
});
return { notified: true };
</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow sourceRef="NotifyDualControlError" targetRef="SecondApprover" />
<bpmn:error id="DualControlError" errorCode="DUAL_CONTROL_VIOLATION" />
Best Practices for User Task Error Handling
✅ Do This
// ✅ Validate all form inputs in ServerScript
if (!formData.requiredField) {
throw new BpmnError('VALIDATION_ERROR', 'Required field is missing');
}
// ✅ Use non-interrupting boundaries for validation errors
<bpmn:boundaryEvent cancelActivity="false">
<bpmn:errorEventDefinition/>
</bpmn:boundaryEvent>
// ✅ Provide helpful error messages
throw new BpmnError('AMOUNT_EXCEEDS_LIMIT',
'Loan amount $' + amount + ' exceeds your limit of $' + maxAmount);
// ✅ Check business rules before proceeding
if (userRole !== 'MANAGER' && amount > 50000) {
throw new BpmnError('INSUFFICIENT_AUTHORITY', 'Amount requires manager approval');
}
// ✅ Set timeouts for user tasks
<bpmn:boundaryEvent attachedToRef="UserTask">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>P7D</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
❌ Don't Do This
// ❌ Silent validation failures
if (!formData.email) {
return { error: 'Invalid email' }; // Should throw BpmnError
}
// ❌ Generic error messages
throw new BpmnError('ERROR', 'Bad input'); // Not helpful
// ❌ No timeouts
<!-- User task with no timeout - could wait forever -->
// ❌ Interrupting boundaries for recoverable errors
<bpmn:boundaryEvent cancelActivity="true"> <!-- Should be false for validation -->
<bpmn:errorEventDefinition/>
</bpmn:boundaryEvent>
// ❌ No error handling
<!-- No boundary events - validation errors crash process -->
Related Documentation
- JavaScript Error Throwing Guide - Complete BpmnError reference
- Timer Events - Timeout patterns (Phase 4)
- Async Boundaries - Background processing (Phase 2)
- Error Recovery Patterns - Advanced recovery strategies
Error Handling
Handle cases where user task fails or times out:
<bpmn:userTask id="Task_Review">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="review-form" />
<custom:property name="TimeoutMs" value="86400000" /> <!-- 24 hours -->
<custom:property name="OnTimeout" value="EscalateToManager" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Boundary event for timeout -->
<bpmn:boundaryEvent id="Event_Timeout" attachedToRef="Task_Review">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT24H</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<bpmn:sequenceFlow sourceRef="Event_Timeout" targetRef="Task_EscalateToManager" />
Next Steps
- ServiceTask - Call external APIs
- Gateway - Route based on user actions
- Examples - Complete approval workflows
- Advanced Error Handling - Deep dive into error patterns