Skip to main content

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 rendering
  • TaskType: Must be "UserTask"
  • Name: Display name of the task

Optional Properties

  • Script (PreScript): JavaScript code executed before task pauses, prepares formContext for UI
  • ServerScript: JavaScript code executed after user submits form, validates formData
  • ClientScript: JavaScript code for client-side UI logic (runs in browser)
  • ResultVariable: Variable name to store ServerScript result
  • UserActions: Array of available actions (e.g., ["Approve", "Reject"])
  • EntityState: State to set when task becomes active
  • ResponsibleTeams: Teams assigned to this task
  • ResponsibleUsers: Specific users assigned to this task
  • Description: Task description
  • InputMapping: Map process variables to task inputs
  • OutputMapping: Map task outputs to process variables
Script Execution Flow

UserTask supports three script types that execute at different stages:

  1. PreScript (Script property): Runs when task is reached, before pause → prepares formContext
  2. ClientScript (ClientScript property): Runs in browser for UI logic
  3. ServerScript (ServerScript property): Runs after signal/resume, validates formData

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 &lt; 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:

  1. Execute PreScript - Runs the Script property with current process variables
  2. Store Form Context - Result is stored as formContext in the waiting task
  3. Pause Process - Process status changes to "Waiting", execution stops
  4. 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 &lt; 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' &amp;&amp; loanAmount > 500000 &amp;&amp; !comments) {
throw new Error('Approval comments are required for loans over $500,000');
}

if (action === 'Reject' &amp;&amp; !comments) {
throw new Error('Rejection reason is required');
}

// Additional business logic validation
if (action === 'Approve' &amp;&amp; creditScore &lt; 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):

  1. Merge Form Data - All submitted form fields are merged into the process context
  2. Execute ServerScript - Runs with full context including submitted formData
  3. Store Result - ServerScript result stored in ResultVariable (or lastScriptResult if not specified)
  4. 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="{
&quot;applicationAmount&quot;: &quot;loanAmount&quot;,
&quot;customerInfo&quot;: &quot;customer&quot;,
&quot;riskAssessment&quot;: &quot;riskScore&quot;
}" />

<!-- Output mapping: Write to process variables -->
<custom:property name="OutputMapping" value="{
&quot;approvalDecision&quot;: &quot;decision&quot;,
&quot;approverComments&quot;: &quot;comments&quot;,
&quot;approvalTimestamp&quot;: &quot;timestamp&quot;
}" />
</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="{
&quot;documents&quot;: &quot;uploadedDocuments&quot;,
&quot;customerName&quot;: &quot;customer.name&quot;
}" />
<custom:property name="OutputMapping" value="{
&quot;verificationStatus&quot;: &quot;status&quot;,
&quot;verifiedBy&quot;: &quot;verifier&quot;,
&quot;verifiedDocuments&quot;: &quot;verified&quot;
}" />
</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="{
&quot;officerRecommendation&quot;: &quot;officerComments&quot;
}" />
</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="{
&quot;displayAmount&quot;: &quot;loanAmount&quot;,
&quot;customerSummary&quot;: &quot;customer.summary&quot;
}" />

<custom:property name="OutputMapping" value="{
&quot;approvalResult&quot;: &quot;result&quot;,
&quot;approverNotes&quot;: &quot;notes&quot;
}" />

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 Script property 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 ClientScript property contains JavaScript code that runs in the browser.
  • It can use the variables in WorkflowData to 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 Script to 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:
    1. If Script is present, it executes it and returns only those variables.
    2. If FormVariables is present, it returns only those variables.
    3. Otherwise, returns an empty object.
  • This is implemented in the PrepareFormContext method 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 -->

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