Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1285 lines
36 KiB
Markdown
1285 lines
36 KiB
Markdown
# Common Workflow Patterns: Dynamic Workflow UI System
|
|
|
|
## Overview
|
|
|
|
This document provides examples of common workflow patterns that can be implemented using the Dynamic Workflow UI System. These patterns serve as templates and best practices for building workflows that leverage the system's capabilities.
|
|
|
|
> **Important Note on Workflow Execution**: Workflows are executed in response to events. When a workflow is triggered, the event that triggered it is passed as input to the workflow. The workflow does not wait for the initial event - the fact that the workflow is executing means the event has already occurred. This is reflected in the patterns below where each workflow receives its triggering event via `context.input.triggerEvent`.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Approval Workflows](#approval-workflows)
|
|
- [Simple Approval](#simple-approval)
|
|
- [Multi-Level Approval](#multi-level-approval)
|
|
- [Parallel Approval](#parallel-approval)
|
|
- [Conditional Approval](#conditional-approval)
|
|
|
|
2. [Request and Fulfillment Workflows](#request-and-fulfillment-workflows)
|
|
- [Service Request](#service-request)
|
|
- [Credit Reimbursement](#credit-reimbursement)
|
|
- [Resource Allocation](#resource-allocation)
|
|
|
|
3. [Review and Feedback Workflows](#review-and-feedback-workflows)
|
|
- [Document Review](#document-review)
|
|
- [Performance Review](#performance-review)
|
|
- [Quality Assurance](#quality-assurance)
|
|
|
|
4. [Onboarding and Provisioning Workflows](#onboarding-and-provisioning-workflows)
|
|
- [Customer Onboarding](#customer-onboarding)
|
|
- [Employee Onboarding](#employee-onboarding)
|
|
- [System Provisioning](#system-provisioning)
|
|
|
|
5. [Incident Management Workflows](#incident-management-workflows)
|
|
- [Issue Triage](#issue-triage)
|
|
- [Incident Response](#incident-response)
|
|
- [Problem Management](#problem-management)
|
|
|
|
## Approval Workflows
|
|
|
|
### Simple Approval
|
|
|
|
A basic workflow where a request is submitted and approved or rejected by a single approver.
|
|
|
|
#### Workflow Definition
|
|
|
|
```typescript
|
|
|
|
/**
|
|
* Simple Approval Workflow
|
|
*
|
|
* A basic workflow where a request is submitted and approved or rejected by a single approver.
|
|
*
|
|
* @param context The workflow context provided by the runtime
|
|
*/
|
|
async function simpleApprovalWorkflow(context): Promise<void> {
|
|
const { actions, events, data, logger } = context;
|
|
|
|
// Initial state - Processing
|
|
context.setState('processing');
|
|
|
|
// The workflow is triggered by a Submit event, which is passed as input
|
|
const { triggerEvent } = context.input;
|
|
logger.info(`Processing request submitted by ${triggerEvent.user_id}`);
|
|
|
|
// Store request data
|
|
data.set('requestData', triggerEvent.payload);
|
|
data.set('requestor', triggerEvent.user_id);
|
|
|
|
// Create approval task
|
|
const { taskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Approve Request',
|
|
description: `Please review and approve the request submitted by ${submitEvent.user_id}`,
|
|
priority: 'medium',
|
|
dueDate: '2 days',
|
|
assignTo: {
|
|
roles: ['manager']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id
|
|
}
|
|
});
|
|
|
|
// Update state
|
|
context.setState('pending_approval');
|
|
|
|
// Wait for task completion
|
|
const approvalEvent = await events.waitFor(`Task:${taskId}:Complete`);
|
|
|
|
// Process approval decision
|
|
const { approved, comments } = approvalEvent.payload;
|
|
|
|
if (approved) {
|
|
// Handle approval
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_approved',
|
|
data: {
|
|
comments
|
|
}
|
|
});
|
|
|
|
context.setState('approved');
|
|
} else {
|
|
// Handle rejection
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected');
|
|
}
|
|
|
|
logger.info('Workflow completed');
|
|
}
|
|
```
|
|
|
|
#### Form Definition
|
|
|
|
```typescript
|
|
// Approval form definition
|
|
const approvalForm = {
|
|
formId: 'simple-approval-form',
|
|
name: 'Simple Approval Form',
|
|
description: 'Form for approving or rejecting a request',
|
|
version: '1.0.0',
|
|
category: 'approval',
|
|
status: FormStatus.ACTIVE,
|
|
jsonSchema: {
|
|
type: 'object',
|
|
required: ['approved'],
|
|
properties: {
|
|
approved: {
|
|
type: 'boolean',
|
|
title: 'Approve this request?',
|
|
default: false
|
|
},
|
|
comments: {
|
|
type: 'string',
|
|
title: 'Comments',
|
|
description: 'Provide any comments or feedback'
|
|
}
|
|
}
|
|
},
|
|
uiSchema: {
|
|
approved: {
|
|
'ui:widget': 'checkbox'
|
|
},
|
|
comments: {
|
|
'ui:widget': 'textarea',
|
|
'ui:options': {
|
|
rows: 5
|
|
}
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Multi-Level Approval
|
|
|
|
A workflow where a request requires approval from multiple levels of management in sequence.
|
|
|
|
#### Workflow Definition
|
|
|
|
```typescript
|
|
|
|
/**
|
|
* Multi-Level Approval Workflow
|
|
*
|
|
* A workflow where a request requires approval from multiple levels of management in sequence.
|
|
*
|
|
* @param context The workflow context provided by the runtime
|
|
*/
|
|
async function multiLevelApprovalWorkflow(context): Promise<void> {
|
|
const { actions, events, data, logger } = context;
|
|
|
|
// Initial state - Processing
|
|
context.setState('processing');
|
|
|
|
// The workflow is triggered by a Submit event, which is passed as input
|
|
const { triggerEvent } = context.input;
|
|
|
|
// Store request data
|
|
data.set('requestData', triggerEvent.payload);
|
|
data.set('requestor', triggerEvent.user_id);
|
|
|
|
// First level approval (Team Lead)
|
|
context.setState('pending_team_lead_approval');
|
|
|
|
const { taskId: teamLeadTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Team Lead Approval',
|
|
description: 'First level approval by Team Lead',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['team_lead']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id
|
|
}
|
|
});
|
|
|
|
// Wait for team lead decision
|
|
const teamLeadEvent = await events.waitFor(`Task:${teamLeadTaskId}:Complete`);
|
|
|
|
// If rejected by team lead, end workflow
|
|
if (!teamLeadEvent.payload.approved) {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
level: 'Team Lead',
|
|
comments: teamLeadEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected_by_team_lead');
|
|
return;
|
|
}
|
|
|
|
// Store team lead approval
|
|
data.set('teamLeadApproval', {
|
|
approver: teamLeadEvent.user_id,
|
|
timestamp: teamLeadEvent.timestamp,
|
|
comments: teamLeadEvent.payload.comments
|
|
});
|
|
|
|
// Second level approval (Manager)
|
|
context.setState('pending_manager_approval');
|
|
|
|
const { taskId: managerTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Manager Approval',
|
|
description: 'Second level approval by Manager',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['manager']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id,
|
|
teamLeadApproval: data.get('teamLeadApproval')
|
|
}
|
|
});
|
|
|
|
// Wait for manager decision
|
|
const managerEvent = await events.waitFor(`Task:${managerTaskId}:Complete`);
|
|
|
|
// If rejected by manager, end workflow
|
|
if (!managerEvent.payload.approved) {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
level: 'Manager',
|
|
comments: managerEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected_by_manager');
|
|
return;
|
|
}
|
|
|
|
// Store manager approval
|
|
data.set('managerApproval', {
|
|
approver: managerEvent.user_id,
|
|
timestamp: managerEvent.timestamp,
|
|
comments: managerEvent.payload.comments
|
|
});
|
|
|
|
// For high-value requests, require director approval
|
|
if (submitEvent.payload.amount > 10000) {
|
|
// Third level approval (Director)
|
|
context.setState('pending_director_approval');
|
|
|
|
const { taskId: directorTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Director Approval',
|
|
description: 'Third level approval by Director (required for high-value requests)',
|
|
priority: 'high',
|
|
assignTo: {
|
|
roles: ['director']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id,
|
|
teamLeadApproval: data.get('teamLeadApproval'),
|
|
managerApproval: data.get('managerApproval')
|
|
}
|
|
});
|
|
|
|
// Wait for director decision
|
|
const directorEvent = await events.waitFor(`Task:${directorTaskId}:Complete`);
|
|
|
|
// If rejected by director, end workflow
|
|
if (!directorEvent.payload.approved) {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
level: 'Director',
|
|
comments: directorEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected_by_director');
|
|
return;
|
|
}
|
|
|
|
// Store director approval
|
|
data.set('directorApproval', {
|
|
approver: directorEvent.user_id,
|
|
timestamp: directorEvent.timestamp,
|
|
comments: directorEvent.payload.comments
|
|
});
|
|
}
|
|
|
|
// All required approvals received
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_approved',
|
|
data: {
|
|
approvals: [
|
|
data.get('teamLeadApproval'),
|
|
data.get('managerApproval'),
|
|
data.get('directorApproval')
|
|
].filter(Boolean)
|
|
}
|
|
});
|
|
|
|
// Execute the approved request
|
|
await actions.executeApprovedRequest({
|
|
requestData: data.get('requestData'),
|
|
approvals: [
|
|
data.get('teamLeadApproval'),
|
|
data.get('managerApproval'),
|
|
data.get('directorApproval')
|
|
].filter(Boolean)
|
|
});
|
|
|
|
context.setState('approved');
|
|
logger.info('Multi-level approval workflow completed');
|
|
}
|
|
```
|
|
|
|
### Parallel Approval
|
|
|
|
A workflow where multiple approvers must review a request simultaneously, and all must approve for the request to proceed.
|
|
|
|
#### Workflow Definition
|
|
|
|
```typescript
|
|
|
|
/**
|
|
* Parallel Approval Workflow
|
|
*
|
|
* A workflow where multiple approvers must review a request simultaneously,
|
|
* and all must approve for the request to proceed.
|
|
*
|
|
* @param context The workflow context provided by the runtime
|
|
*/
|
|
async function parallelApprovalWorkflow(context): Promise<void> {
|
|
const { actions, events, data, logger } = context;
|
|
|
|
// Initial state - Processing
|
|
context.setState('processing');
|
|
|
|
// The workflow is triggered by a Submit event, which is passed as input
|
|
const { triggerEvent } = context.input;
|
|
|
|
// Store request data
|
|
data.set('requestData', triggerEvent.payload);
|
|
data.set('requestor', triggerEvent.user_id);
|
|
|
|
// Create approval tasks for all required approvers
|
|
context.setState('pending_approval');
|
|
|
|
// Create financial approval task
|
|
const { taskId: financialTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Financial Approval',
|
|
description: 'Financial review and approval',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['financial_approver']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id,
|
|
approvalType: 'financial'
|
|
}
|
|
});
|
|
|
|
// Create technical approval task
|
|
const { taskId: technicalTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Technical Approval',
|
|
description: 'Technical review and approval',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['technical_approver']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id,
|
|
approvalType: 'technical'
|
|
}
|
|
});
|
|
|
|
// Create legal approval task
|
|
const { taskId: legalTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Legal Approval',
|
|
description: 'Legal review and approval',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['legal_approver']
|
|
},
|
|
contextData: {
|
|
requestData: submitEvent.payload,
|
|
requestor: submitEvent.user_id,
|
|
approvalType: 'legal'
|
|
}
|
|
});
|
|
|
|
// Store task IDs
|
|
data.set('approvalTasks', {
|
|
financial: financialTaskId,
|
|
technical: technicalTaskId,
|
|
legal: legalTaskId
|
|
});
|
|
|
|
// Initialize approval status
|
|
data.set('approvalStatus', {
|
|
financial: null,
|
|
technical: null,
|
|
legal: null
|
|
});
|
|
|
|
// Wait for all approvals in parallel
|
|
await Promise.all([
|
|
(async () => {
|
|
const financialEvent = await events.waitFor(`Task:${financialTaskId}:Complete`);
|
|
const approved = financialEvent.payload.approved;
|
|
|
|
// Update approval status
|
|
const approvalStatus = data.get('approvalStatus');
|
|
approvalStatus.financial = {
|
|
approved,
|
|
approver: financialEvent.user_id,
|
|
timestamp: financialEvent.timestamp,
|
|
comments: financialEvent.payload.comments
|
|
};
|
|
data.set('approvalStatus', approvalStatus);
|
|
|
|
logger.info(`Financial approval: ${approved ? 'Approved' : 'Rejected'}`);
|
|
})(),
|
|
|
|
(async () => {
|
|
const technicalEvent = await events.waitFor(`Task:${technicalTaskId}:Complete`);
|
|
const approved = technicalEvent.payload.approved;
|
|
|
|
// Update approval status
|
|
const approvalStatus = data.get('approvalStatus');
|
|
approvalStatus.technical = {
|
|
approved,
|
|
approver: technicalEvent.user_id,
|
|
timestamp: technicalEvent.timestamp,
|
|
comments: technicalEvent.payload.comments
|
|
};
|
|
data.set('approvalStatus', approvalStatus);
|
|
|
|
logger.info(`Technical approval: ${approved ? 'Approved' : 'Rejected'}`);
|
|
})(),
|
|
|
|
(async () => {
|
|
const legalEvent = await events.waitFor(`Task:${legalTaskId}:Complete`);
|
|
const approved = legalEvent.payload.approved;
|
|
|
|
// Update approval status
|
|
const approvalStatus = data.get('approvalStatus');
|
|
approvalStatus.legal = {
|
|
approved,
|
|
approver: legalEvent.user_id,
|
|
timestamp: legalEvent.timestamp,
|
|
comments: legalEvent.payload.comments
|
|
};
|
|
data.set('approvalStatus', approvalStatus);
|
|
|
|
logger.info(`Legal approval: ${approved ? 'Approved' : 'Rejected'}`);
|
|
})()
|
|
]);
|
|
|
|
// Check if all approvals were received
|
|
const approvalStatus = data.get('approvalStatus');
|
|
const allApproved = Object.values(approvalStatus).every(status => status && status.approved);
|
|
|
|
if (allApproved) {
|
|
// All approvers approved
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_approved',
|
|
data: {
|
|
approvals: Object.values(approvalStatus)
|
|
}
|
|
});
|
|
|
|
// Execute the approved request
|
|
await actions.executeApprovedRequest({
|
|
requestData: data.get('requestData'),
|
|
approvals: Object.values(approvalStatus)
|
|
});
|
|
|
|
context.setState('approved');
|
|
} else {
|
|
// At least one approver rejected
|
|
const rejections = Object.entries(approvalStatus)
|
|
.filter(([_, status]) => status && !status.approved)
|
|
.map(([type, status]) => ({
|
|
type,
|
|
...status
|
|
}));
|
|
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
rejections
|
|
}
|
|
});
|
|
|
|
context.setState('rejected');
|
|
}
|
|
|
|
logger.info('Parallel approval workflow completed');
|
|
}
|
|
```
|
|
|
|
### Conditional Approval
|
|
|
|
A workflow where the approval path depends on the request attributes, such as amount, category, or risk level.
|
|
|
|
#### Workflow Definition
|
|
|
|
```typescript
|
|
|
|
/**
|
|
* Conditional Approval Workflow
|
|
*
|
|
* A workflow where the approval path depends on the request attributes,
|
|
* such as amount, category, or risk level.
|
|
*
|
|
* @param context The workflow context provided by the runtime
|
|
*/
|
|
async function conditionalApprovalWorkflow(context): Promise<void> {
|
|
const { actions, events, data, logger } = context;
|
|
|
|
// Initial state - Processing
|
|
context.setState('processing');
|
|
|
|
// The workflow is triggered by a Submit event, which is passed as input
|
|
const { triggerEvent } = context.input;
|
|
const requestData = triggerEvent.payload;
|
|
|
|
// Store request data
|
|
data.set('requestData', requestData);
|
|
data.set('requestor', triggerEvent.user_id);
|
|
|
|
// Determine approval path based on request attributes
|
|
let approvalPath;
|
|
|
|
if (requestData.amount <= 1000) {
|
|
// Low value - requires only team lead approval
|
|
approvalPath = 'team_lead_only';
|
|
} else if (requestData.amount <= 10000) {
|
|
// Medium value - requires team lead and manager approval
|
|
approvalPath = 'team_lead_and_manager';
|
|
} else {
|
|
// High value - requires team lead, manager, and director approval
|
|
approvalPath = 'full_approval_chain';
|
|
}
|
|
|
|
// Additional conditions
|
|
if (requestData.category === 'legal') {
|
|
// Legal requests always require legal review
|
|
approvalPath = 'legal_review';
|
|
} else if (requestData.risk_level === 'high') {
|
|
// High risk requests always require full approval chain
|
|
approvalPath = 'full_approval_chain';
|
|
}
|
|
|
|
// Store approval path
|
|
data.set('approvalPath', approvalPath);
|
|
logger.info(`Selected approval path: ${approvalPath}`);
|
|
|
|
// Execute the selected approval path
|
|
switch (approvalPath) {
|
|
case 'team_lead_only':
|
|
await executeTeamLeadOnlyPath(context);
|
|
break;
|
|
case 'team_lead_and_manager':
|
|
await executeTeamLeadAndManagerPath(context);
|
|
break;
|
|
case 'legal_review':
|
|
await executeLegalReviewPath(context);
|
|
break;
|
|
case 'full_approval_chain':
|
|
await executeFullApprovalChainPath(context);
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown approval path: ${approvalPath}`);
|
|
}
|
|
|
|
logger.info('Conditional approval workflow completed');
|
|
}
|
|
|
|
// Helper functions for different approval paths
|
|
|
|
async function executeTeamLeadOnlyPath(context): Promise<void> {
|
|
const { actions, events, data } = context;
|
|
|
|
context.setState('pending_team_lead_approval');
|
|
|
|
const { taskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Team Lead Approval',
|
|
description: 'Approval for low-value request',
|
|
priority: 'low',
|
|
assignTo: {
|
|
roles: ['team_lead']
|
|
},
|
|
contextData: {
|
|
requestData: data.get('requestData'),
|
|
requestor: data.get('requestor'),
|
|
approvalPath: 'team_lead_only'
|
|
}
|
|
});
|
|
|
|
const approvalEvent = await events.waitFor(`Task:${taskId}:Complete`);
|
|
|
|
if (approvalEvent.payload.approved) {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_approved',
|
|
data: {
|
|
approver: approvalEvent.user_id,
|
|
comments: approvalEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
await actions.executeApprovedRequest({
|
|
requestData: data.get('requestData'),
|
|
approval: {
|
|
approver: approvalEvent.user_id,
|
|
timestamp: approvalEvent.timestamp,
|
|
comments: approvalEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('approved');
|
|
} else {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
approver: approvalEvent.user_id,
|
|
comments: approvalEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected');
|
|
}
|
|
}
|
|
|
|
async function executeTeamLeadAndManagerPath(context): Promise<void> {
|
|
const { actions, events, data } = context;
|
|
|
|
// Team Lead approval
|
|
context.setState('pending_team_lead_approval');
|
|
|
|
const { taskId: teamLeadTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Team Lead Approval',
|
|
description: 'First level approval for medium-value request',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['team_lead']
|
|
},
|
|
contextData: {
|
|
requestData: data.get('requestData'),
|
|
requestor: data.get('requestor'),
|
|
approvalPath: 'team_lead_and_manager'
|
|
}
|
|
});
|
|
|
|
const teamLeadEvent = await events.waitFor(`Task:${teamLeadTaskId}:Complete`);
|
|
|
|
if (!teamLeadEvent.payload.approved) {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
level: 'Team Lead',
|
|
comments: teamLeadEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected_by_team_lead');
|
|
return;
|
|
}
|
|
|
|
// Manager approval
|
|
context.setState('pending_manager_approval');
|
|
|
|
const { taskId: managerTaskId } = await actions.createHumanTask({
|
|
taskType: 'approval',
|
|
title: 'Manager Approval',
|
|
description: 'Second level approval for medium-value request',
|
|
priority: 'medium',
|
|
assignTo: {
|
|
roles: ['manager']
|
|
},
|
|
contextData: {
|
|
requestData: data.get('requestData'),
|
|
requestor: data.get('requestor'),
|
|
teamLeadApproval: {
|
|
approver: teamLeadEvent.user_id,
|
|
timestamp: teamLeadEvent.timestamp,
|
|
comments: teamLeadEvent.payload.comments
|
|
}
|
|
}
|
|
});
|
|
|
|
const managerEvent = await events.waitFor(`Task:${managerTaskId}:Complete`);
|
|
|
|
if (managerEvent.payload.approved) {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_approved',
|
|
data: {
|
|
approvals: [
|
|
{
|
|
level: 'Team Lead',
|
|
approver: teamLeadEvent.user_id,
|
|
comments: teamLeadEvent.payload.comments
|
|
},
|
|
{
|
|
level: 'Manager',
|
|
approver: managerEvent.user_id,
|
|
comments: managerEvent.payload.comments
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
await actions.executeApprovedRequest({
|
|
requestData: data.get('requestData'),
|
|
approvals: [
|
|
{
|
|
level: 'Team Lead',
|
|
approver: teamLeadEvent.user_id,
|
|
timestamp: teamLeadEvent.timestamp,
|
|
comments: teamLeadEvent.payload.comments
|
|
},
|
|
{
|
|
level: 'Manager',
|
|
approver: managerEvent.user_id,
|
|
timestamp: managerEvent.timestamp,
|
|
comments: managerEvent.payload.comments
|
|
}
|
|
]
|
|
});
|
|
|
|
context.setState('approved');
|
|
} else {
|
|
await actions.sendNotification({
|
|
recipient: data.get('requestor'),
|
|
template: 'request_rejected',
|
|
data: {
|
|
level: 'Manager',
|
|
comments: managerEvent.payload.comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected_by_manager');
|
|
}
|
|
}
|
|
|
|
// Additional approval path implementations would follow the same pattern
|
|
```
|
|
|
|
## Request and Fulfillment Workflows
|
|
|
|
### Credit Reimbursement
|
|
|
|
A workflow for processing credit reimbursement requests.
|
|
|
|
#### Workflow Definition
|
|
|
|
```typescript
|
|
/**
|
|
* Credit Reimbursement Workflow
|
|
*
|
|
* A workflow for processing credit reimbursement requests.
|
|
*
|
|
* @param context The workflow context provided by the runtime
|
|
*/
|
|
async function creditReimbursementWorkflow(context): Promise<void> {
|
|
const { actions, events, data, logger } = context;
|
|
|
|
// Initial state
|
|
context.setState('processing');
|
|
|
|
// The workflow is triggered by a Submit event, which is passed as input
|
|
const { triggerEvent } = context.input;
|
|
const requestData = triggerEvent.payload;
|
|
|
|
// Store request data
|
|
data.set('requestData', requestData);
|
|
data.set('requestor', triggerEvent.user_id);
|
|
data.set('submissionDate', triggerEvent.timestamp);
|
|
|
|
// Validate customer information
|
|
context.setState('validating');
|
|
|
|
const validationResult = await actions.validateCustomerInformation({
|
|
customerId: requestData.customer,
|
|
amount: requestData.amount
|
|
});
|
|
|
|
if (!validationResult.valid) {
|
|
// Customer validation failed
|
|
await actions.sendNotification({
|
|
recipient: submitEvent.user_id,
|
|
template: 'validation_failed',
|
|
data: {
|
|
reason: validationResult.reason
|
|
}
|
|
});
|
|
|
|
context.setState('validation_failed');
|
|
return;
|
|
}
|
|
|
|
// Store customer information
|
|
data.set('customerInfo', validationResult.customerInfo);
|
|
|
|
// Create approval task
|
|
context.setState('pending_approval');
|
|
|
|
const { taskId } = await actions.createHumanTask({
|
|
taskType: 'credit_approval',
|
|
title: 'Approve Credit Reimbursement',
|
|
description: `Review and approve credit reimbursement request for ${requestData.customer}`,
|
|
priority: requestData.amount > 1000 ? 'high' : 'medium',
|
|
dueDate: '3 days',
|
|
assignTo: {
|
|
roles: ['finance_approver']
|
|
},
|
|
contextData: {
|
|
requestData,
|
|
customerInfo: validationResult.customerInfo
|
|
}
|
|
});
|
|
|
|
// Wait for approval decision
|
|
const approvalEvent = await events.waitFor(`Task:${taskId}:Complete`);
|
|
const { approved, adjustedAmount, reason, comments } = approvalEvent.payload;
|
|
|
|
if (!approved) {
|
|
// Request rejected
|
|
await actions.sendNotification({
|
|
recipient: submitEvent.user_id,
|
|
template: 'reimbursement_rejected',
|
|
data: {
|
|
reason,
|
|
comments
|
|
}
|
|
});
|
|
|
|
context.setState('rejected');
|
|
return;
|
|
}
|
|
|
|
// Store approval information
|
|
data.set('approvalInfo', {
|
|
approver: approvalEvent.user_id,
|
|
timestamp: approvalEvent.timestamp,
|
|
adjustedAmount: adjustedAmount || requestData.amount,
|
|
comments
|
|
});
|
|
|
|
// Process reimbursement
|
|
context.setState('processing');
|
|
|
|
const finalAmount = adjustedAmount || requestData.amount;
|
|
|
|
const processingResult = await actions.processReimbursement({
|
|
customerId: requestData.customer,
|
|
amount: finalAmount,
|
|
reason: requestData.reason,
|
|
approver: approvalEvent.user_id,
|
|
reference: `REIMB-${context.executionId}`
|
|
});
|
|
|
|
if (processingResult.success) {
|
|
// Reimbursement processed successfully
|
|
await actions.sendNotification({
|
|
recipient: submitEvent.user_id,
|
|
template: 'reimbursement_processed',
|
|
data: {
|
|
amount: finalAmount,
|
|
transactionId: processingResult.transactionId,
|
|
processingDate: processingResult.timestamp
|
|
}
|
|
});
|
|
|
|
// Send notification to customer
|
|
await actions.sendNotification({
|
|
recipient: validationResult.customerInfo.email,
|
|
template: 'customer_reimbursement_notification',
|
|
data: {
|
|
amount: finalAmount,
|
|
reason: requestData.reason,
|
|
processingDate: processingResult.timestamp
|
|
}
|
|
});
|
|
|
|
// Update accounting records
|
|
await actions.updateAccountingRecords({
|
|
type: 'credit_reimbursement',
|
|
customerId: requestData.customer,
|
|
amount: finalAmount,
|
|
transactionId: processingResult.transactionId,
|
|
approver: approvalEvent.user_id
|
|
});
|
|
|
|
context.setState('completed');
|
|
} else {
|
|
// Reimbursement processing failed
|
|
await actions.sendNotification({
|
|
recipient: submitEvent.user_id,
|
|
template: 'reimbursement_failed',
|
|
data: {
|
|
reason: processingResult.reason
|
|
}
|
|
});
|
|
|
|
// Create manual intervention task
|
|
await actions.createHumanTask({
|
|
taskType: 'manual_intervention',
|
|
title: 'Manual Reimbursement Processing Required',
|
|
description: `Automated reimbursement processing failed for ${requestData.customer}. Manual intervention required.`,
|
|
priority: 'high',
|
|
dueDate: '1 day',
|
|
assignTo: {
|
|
roles: ['finance_operations']
|
|
},
|
|
contextData: {
|
|
requestData,
|
|
customerInfo: validationResult.customerInfo,
|
|
approvalInfo: data.get('approvalInfo'),
|
|
processingError: processingResult.reason
|
|
}
|
|
});
|
|
|
|
context.setState('manual_intervention_required');
|
|
}
|
|
|
|
logger.info('Credit reimbursement workflow completed');
|
|
}
|
|
```
|
|
|
|
#### Form Definitions
|
|
|
|
```typescript
|
|
// Credit reimbursement request form
|
|
const creditReimbursementRequestForm = {
|
|
formId: 'credit-reimbursement-request',
|
|
name: 'Credit Reimbursement Request',
|
|
description: 'Form for requesting credit reimbursements',
|
|
version: '1.0.0',
|
|
category: 'finance',
|
|
status: FormStatus.ACTIVE,
|
|
jsonSchema: {
|
|
type: 'object',
|
|
required: ['customer', 'amount', 'reason'],
|
|
properties: {
|
|
customer: {
|
|
type: 'string',
|
|
title: 'Customer Name'
|
|
},
|
|
amount: {
|
|
type: 'number',
|
|
title: 'Amount',
|
|
minimum: 0
|
|
},
|
|
reason: {
|
|
type: 'string',
|
|
title: 'Reason for Reimbursement'
|
|
},
|
|
date: {
|
|
type: 'string',
|
|
format: 'date',
|
|
title: 'Date of Transaction'
|
|
},
|
|
orderNumber: {
|
|
type: 'string',
|
|
title: 'Order Number (if applicable)'
|
|
}
|
|
}
|
|
},
|
|
uiSchema: {
|
|
customer: {
|
|
'ui:widget': 'CompanyPickerWidget',
|
|
'ui:autofocus': true
|
|
},
|
|
amount: {
|
|
'ui:widget': 'currencyWidget'
|
|
},
|
|
reason: {
|
|
'ui:widget': 'textarea'
|
|
},
|
|
date: {
|
|
'ui:widget': 'date'
|
|
}
|
|
}
|
|
};
|
|
|
|
// Credit approval form
|
|
const creditApprovalForm = {
|
|
formId: 'credit-approval-form',
|
|
name: 'Credit Approval Form',
|
|
description: 'Form for approving credit reimbursements',
|
|
version: '1.0.0',
|
|
category: 'finance',
|
|
status: FormStatus.ACTIVE,
|
|
jsonSchema: {
|
|
type: 'object',
|
|
required: ['approved'],
|
|
properties: {
|
|
approved: {
|
|
type: 'boolean',
|
|
title: 'Approve this reimbursement?',
|
|
default: false
|
|
},
|
|
adjustedAmount: {
|
|
type: 'number',
|
|
title: 'Adjusted Amount (if different from requested amount)'
|
|
},
|
|
reason: {
|
|
type: 'string',
|
|
title: 'Reason for Adjustment/Rejection'
|
|
},
|
|
comments: {
|
|
type: 'string',
|
|
title: 'Comments'
|
|
}
|
|
}
|
|
},
|
|
uiSchema: {
|
|
approved: {
|
|
'ui:widget': 'checkbox'
|
|
},
|
|
adjustedAmount: {
|
|
'ui:widget': 'currencyWidget',
|
|
'ui:displayIf': {
|
|
field: 'approved',
|
|
value: true
|
|
}
|
|
},
|
|
reason: {
|
|
'ui:widget': 'textarea',
|
|
'ui:displayIf': {
|
|
or: [
|
|
{ field: 'approved', value: false },
|
|
{ field: 'adjustedAmount', not: null }
|
|
]
|
|
}
|
|
},
|
|
comments: {
|
|
'ui:widget': 'textarea'
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
## Review and Feedback Workflows
|
|
|
|
### Document Review
|
|
|
|
A workflow for reviewing and approving documents.
|
|
|
|
#### Workflow Definition
|
|
|
|
```typescript
|
|
/**
|
|
* Document Review Workflow
|
|
*
|
|
* A workflow for reviewing and approving documents.
|
|
*
|
|
* @param context The workflow context provided by the runtime
|
|
*/
|
|
async function documentReviewWorkflow(context): Promise<void> {
|
|
const { actions, events, data, logger } = context;
|
|
|
|
// Initial state
|
|
context.setState('processing');
|
|
|
|
// The workflow is triggered by a Submit event, which is passed as input
|
|
const { triggerEvent } = context.input;
|
|
const documentData = triggerEvent.payload;
|
|
|
|
// Store document data
|
|
data.set('documentData', documentData);
|
|
data.set('author', triggerEvent.user_id);
|
|
data.set('version', '1.0');
|
|
|
|
// Determine reviewers based on document type
|
|
let reviewers;
|
|
|
|
switch (documentData.type) {
|
|
case 'technical':
|
|
reviewers = ['technical_reviewer'];
|
|
break;
|
|
case 'legal':
|
|
reviewers = ['legal_reviewer'];
|
|
break;
|
|
case 'financial':
|
|
reviewers = ['financial_reviewer'];
|
|
break;
|
|
case 'marketing':
|
|
reviewers = ['marketing_reviewer'];
|
|
break;
|
|
default:
|
|
reviewers = ['general_reviewer'];
|
|
}
|
|
|
|
// Add additional reviewers for sensitive documents
|
|
if (documentData.sensitivity === 'high') {
|
|
reviewers.push('compliance_reviewer');
|
|
}
|
|
|
|
// Store reviewers
|
|
data.set('reviewers', reviewers);
|
|
|
|
// Create review tasks for all reviewers
|
|
context.setState('in_review');
|
|
|
|
const reviewTasks = [];
|
|
|
|
for (const reviewer of reviewers) {
|
|
const { taskId } = await actions.createHumanTask({
|
|
taskType: 'document_review',
|
|
title: `Review ${documentData.title}`,
|
|
description: `Please review the document: ${documentData.description}`,
|
|
priority: documentData.priority || 'medium',
|
|
dueDate: documentData.dueDate || '5 days',
|
|
assignTo: {
|
|
roles: [reviewer]
|
|
},
|
|
contextData: {
|
|
documentData,
|
|
author: submitEvent.user_id,
|
|
documentUrl: documentData.url
|
|
}
|
|
});
|
|
|
|
reviewTasks.push({
|
|
taskId,
|
|
reviewer
|
|
});
|
|
}
|
|
|
|
// Store review tasks
|
|
data.set('reviewTasks', reviewTasks);
|
|
|
|
// Initialize review results
|
|
data.set('reviewResults', []);
|
|
|
|
// Wait for all reviews to complete
|
|
for (const task of reviewTasks) {
|
|
const reviewEvent = await events.waitFor(`Task:${task.taskId}:Complete`);
|
|
|
|
// Store review result
|
|
const reviewResults = data.get('reviewResults');
|
|
reviewResults.push({
|
|
reviewer: task.reviewer,
|
|
reviewerId: reviewEvent.user_id,
|
|
timestamp: reviewEvent.timestamp,
|
|
approved: reviewEvent.payload.approved,
|
|
comments: reviewEvent.payload.comments,
|
|
changes: reviewEvent.payload.changes
|
|
});
|
|
data.set('reviewResults', reviewResults);
|
|
|
|
logger.info(`Review completed by ${task.reviewer}: ${reviewEvent.payload.approved ? 'Approved' : 'Changes requested'}`);
|
|
}
|
|
|
|
// Check if all reviewers approved
|
|
const reviewResults = data.get('reviewResults');
|
|
const allApproved = reviewResults.every(result => result.approved);
|
|
|
|
if (allApproved) {
|
|
// All reviewers approved
|
|
await actions.sendNotification({
|
|
recipient: data.get('author'),
|
|
template: 'document_approved',
|
|
data: {
|
|
document: documentData.title,
|
|
reviewers: reviewResults.map(r => r.reviewerId)
|
|
}
|
|
});
|
|
|
|
// Update document status
|
|
await actions.updateDocumentStatus({
|
|
documentId: documentData.id,
|
|
status: 'approved',
|
|
version: data.get('version'),
|
|
approvers: reviewResults.map(r => r.reviewerId)
|
|
});
|
|
|
|
context.setState('approved');
|
|
} else {
|
|
// Changes requested by at least one reviewer
|
|
const changesRequested = reviewResults.filter(result => !result.approved);
|
|
|
|
await actions.sendNotification({
|
|
recipient: data.get('author'),
|
|
template: 'document_changes_requested',
|
|
data: {
|
|
document: documentData.title,
|
|
changesRequested
|
|
}
|
|
});
|
|
|
|
// Update document status
|
|
await actions.updateDocumentStatus({
|
|
documentId: documentData.id,
|
|
status: 'changes_requested',
|
|
version: data.get('version'),
|
|
changesRequested
|
|
});
|
|
|
|
context.setState('changes_requested');
|
|
|
|
// The workflow will be re-triggered when a DocumentRevision event occurs
|
|
// This would be a separate workflow execution with the revision event as input
|
|
logger.info('Waiting for document revision');
|
|
|
|
// Note: In a real implementation, we would end this workflow here
|
|
// and start a new workflow instance when the DocumentRevision event occurs
|
|
|
|
// Restart review process with the same reviewers
|
|
// This could be implemented as a recursive call or by jumping back to the review creation step
|
|
}
|
|
|
|
logger.info('Document review workflow completed');
|
|
}
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
These workflow patterns demonstrate the flexibility and power of the Dynamic Workflow UI System. By leveraging the system's components, you can implement a wide variety of workflows to meet your business needs.
|
|
|
|
Key benefits of using these patterns:
|
|
|
|
1. **Consistency**: Standardized approach to common workflow scenarios
|
|
2. **Reusability**: Patterns can be reused across different business processes
|
|
3. **Maintainability**: Clear separation of concerns makes workflows easier to maintain
|
|
4. **Flexibility**: Patterns can be customized to meet specific requirements
|
|
5. **Scalability**: Patterns can be extended to handle more complex scenarios
|
|
|
|
When implementing these patterns, consider the following best practices:
|
|
|
|
1. **Start Simple**: Begin with the simplest pattern that meets your needs
|
|
2. **Modularize**: Break complex workflows into smaller, reusable components
|
|
3. **Handle Exceptions**: Include error handling and exception paths
|
|
4. **Monitor Performance**: Ensure workflows perform well under load
|
|
5. **Document Decisions**: Document the reasoning behind workflow design decisions
|
|
6. **Test Thoroughly**: Test workflows with various scenarios and edge cases |