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 variablesPermissionLevel: Org-hierarchy filter — restricts who can action the task based on their relationship to the process initiatorNotifyUsers: Whentrue, eligible users are automatically notified (email + in-app) the moment the task enters WAITING stateNotificationTemplate: Optional notification template ID to use for the email notification
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" />
PermissionLevel — Org-Hierarchy Access Control
The PermissionLevel property restricts which users can action a UserTask based on their organisational relationship to the process initiator. This is useful for approval workflows where only staff in the same branch, area, or division as the person who started the process should be able to approve it.
Supported Values
| Value | Description |
|---|---|
None | No restriction — any user with the required permission can action the task (default behaviour) |
SameBranch | Executor must be in the same branch as the initiator |
SameArea | Executor must be in the same area as the initiator |
SameDivision | Executor must be in the same division as the initiator |
SameOrganisation | Executor must belong to the same organisation |
AssociatedOfficer | Reserved for future use — currently grants access to all |
How It Works
When a user attempts to access or signal a waiting UserTask:
- The engine resolves the initiator's branch hierarchy (from
SelfServiceUser.BranchAssociationsor portalUser.BranchId) - The engine resolves the executor's branch hierarchy (the user making the request)
- It compares the two at the specified level (Branch → Area → Division → Organisation)
- If the check fails, a
403 Access Deniedresponse is returned
The engine tries the SelfServiceUser context first (channel/portal customers), then falls back to the admin portal User context. Both share the same SelfServiceArea → SelfServiceDivision → SelfServiceOrganisation hierarchy.
BPMN Example
<bpmn:userTask id="Task_BranchApproval" name="Branch Manager Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="approval-form" />
<custom:property name="UserActions" value="Approve,Reject" />
<custom:property name="Permission" value="LOAN_APPROVE" />
<!-- Only users in the SAME BRANCH as the loan applicant can approve -->
<custom:property name="PermissionLevel" value="SameBranch" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
<!-- Area-level approval: any officer in the same area can approve -->
<custom:property name="PermissionLevel" value="SameArea" />
<!-- Division-level: head office division approves for all its branches -->
<custom:property name="PermissionLevel" value="SameDivision" />
Error Response When Check Fails
{
"isSuccessful": false,
"statusCode": "403",
"message": "Access denied. This task requires SameBranch with the process initiator.",
"data": {
"status": "ACCESS_DENIED",
"errorInfo": {
"code": "PERMISSION_LEVEL_MISMATCH",
"message": "Task requires SameBranch with the initiator",
"reason": "Your organisational branch does not meet the 'SameBranch' requirement",
"requiredLevel": "SameBranch"
}
}
}
NotifyUsers — Automatic Task Notifications
When NotifyUsers is set to true on a UserTask, the engine automatically notifies all eligible users the moment the task enters WAITING state. Eligible users are determined by the task's Permission property (users who hold that permission).
Both an email and an in-app notification are sent.
Properties
| Property | Type | Description |
|---|---|---|
NotifyUsers | bool | Set to true to enable auto-notification when task becomes WAITING |
NotificationTemplate | string | Optional template ID for the email. If omitted, a default message is generated |
BPMN Example
<bpmn:userTask id="Task_CreditApproval" name="Credit Committee Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="credit-approval-form" />
<custom:property name="UserActions" value="Approve,Reject,RequestMoreInfo" />
<custom:property name="Permission" value="CREDIT_APPROVE" />
<!-- Notify all users with CREDIT_APPROVE permission when task becomes active -->
<custom:property name="NotifyUsers" value="true" />
<!-- Optional: use a custom email template -->
<custom:property name="NotificationTemplate" value="credit-approval-notification" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
What Gets Sent
Email notification (via SendMailToGroupCommand):
- Subject:
[{ProcessName}] Task awaiting your action: {TaskName} - Body: Process name, task name, and a link to the task in the admin portal
- Recipients: All users holding the required permission code
In-app notification (via SendNotificationToGroupCommand):
- Title:
Action Required: {TaskName} - Message:
{ProcessName} — {TaskName} is waiting for your action - Action link: Deep link to the process task in the portal
For NotifyUsers to send notifications, the task must have a Permission property set. Without a permission code, the engine has no way to resolve which users to notify and will skip the dispatch.
Using a Custom Email Template
When NotificationTemplate is set, the engine looks up the named template from the HtmlTemplateDefinition table and renders it with a rich context object before sending. This means whoever creates the template in the system has full access to the following variables:
| Variable | Type | Description |
|---|---|---|
processName | string | Display name of the process definition |
taskName | string | Name of the waiting task |
taskId | string | Internal BPMN ID of the task node |
instanceGuid | string | Unique identifier of the running process instance |
processDefinitionId | number | ID of the process definition |
entityType | string | Entity type the process is linked to (e.g. "Loan", "Deposit", "Contact") — null if not linked |
entityId | number | ID of the linked entity record (e.g. loan ID, deposit account ID) — null if not linked |
initiatedByUserId | number | ID of the user who started the process |
link | string | Full deep-link URL to the task in the admin portal, including ?entityType=&entityId= query parameters so the page can load the right entity automatically |
startedAt | datetime | UTC timestamp when the process instance was started |
The exact syntax for referencing variables depends on the template engine used in your HtmlTemplateDefinition. A typical Handlebars/Mustache-style template would look like:
<h2>Action Required: {{taskName}}</h2>
<p>Hello,</p>
<p>
A task in the <strong>{{processName}}</strong> process requires your attention.
</p>
<table>
<tr><td><strong>Task</strong></td><td>{{taskName}}</td></tr>
<tr><td><strong>Entity Type</strong></td><td>{{entityType}}</td></tr>
<tr><td><strong>Entity ID</strong></td><td>{{entityId}}</td></tr>
<tr><td><strong>Started At</strong></td><td>{{startedAt}}</td></tr>
</table>
<p>
<a href="{{link}}">Click here to review and action this task</a>
</p>
The link variable navigates the recipient directly to the process task page and passes ?entityType=&entityId= as query parameters. The portal uses these to pre-load the relevant entity (e.g. open the loan or deposit record) alongside the process task panel — so approvers land in exactly the right context without any extra navigation.
- If
NotificationTemplateis set: the template is rendered with the context above and used as the email body. Thesubjectis still auto-generated. - If
NotificationTemplateis not set: a default HTML message is generated containing the process name, task name, and the deep link.
Combining PermissionLevel with NotifyUsers
You can combine both features. Only users within the correct org-hierarchy level AND with the required permission will be notified:
<bpmn:userTask id="Task_RegionalApproval" name="Regional Manager Approval">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="FormKey" value="regional-approval-form" />
<custom:property name="UserActions" value="Approve,Reject" />
<custom:property name="Permission" value="REGIONAL_APPROVE" />
<!-- Notify eligible users when task becomes active -->
<custom:property name="NotifyUsers" value="true" />
<!-- Only users in the same division as the initiator can action the task -->
<custom:property name="PermissionLevel" value="SameDivision" />
</custom:properties>
</bpmn:extensionElements>
</bpmn:userTask>
Notifications are fire-and-forget — a notification failure will never block the process. Errors are logged as warnings only.
Next Steps
- ServiceTask - Call external APIs
- Gateway - Route based on user actions
- Examples - Complete approval workflows
- Advanced Error Handling - Deep dive into error patterns