FLOW 1: PM/HR Tạo Workflow Template (Process Builder UI)
📝 Step 1: PM mở Process Builder, điền thông tin cơ bản:
// UI Form Input
{
code: "LEAVE_REQUEST",
name: "Đơn xin nghỉ phép",
category: "HR",
icon: "calendar",
description: "Quy trình xin nghỉ phép..."
}
💾 API Call: POST /api/process-definitions
// INSERT INTO ProcessDefinition
{
id: "cuid_abc123",
code: "LEAVE_REQUEST",
name: "Đơn xin nghỉ phép",
category: "HR",
formSchema: {}, // empty, fill later
isActive: false, // draft mode
version: 1,
createdBy: "pm_user_id"
}
📋 Step 2: PM thiết kế Form Schema (drag-drop form builder):
// formSchema được update via PUT /api/process-definitions/:id
{
"type": "object",
"required": ["leaveType", "startDate", "endDate", "reason"],
"properties": {
"leaveType": {
"type": "string",
"enum": ["ANNUAL", "SICK", "UNPAID", "MATERNITY"],
"title": "Loại nghỉ phép"
},
"startDate": {
"type": "string",
"format": "date",
"title": "Ngày bắt đầu"
},
"endDate": {
"type": "string",
"format": "date",
"title": "Ngày kết thúc"
},
"reason": {
"type": "string",
"maxLength": 500,
"title": "Lý do"
},
"attachments": {
"type": "array",
"items": { "type": "string", "format": "uri" },
"title": "Đính kèm (nếu có)"
}
}
}
🔄 Step 3: PM kéo thả Steps trong React Flow canvas:
// Step 1: Manager Approval
POST /api/step-definitions
{
processDefinitionId: "cuid_abc123",
code: "MANAGER_APPROVAL",
name: "Quản lý duyệt",
order: 1,
type: "APPROVAL",
assigneeType: "REQUESTER_MANAGER",
timeoutHours: 48,
escalationRoleId: "hr_admin_role"
}
// Step 2: HR Confirm
POST /api/step-definitions
{
processDefinitionId: "cuid_abc123",
code: "HR_CONFIRM",
name: "HR xác nhận",
order: 2,
type: "APPROVAL",
assigneeType: "ROLE",
assigneeConfig: {
"roleId": "hr_staff_role"
},
timeoutHours: 24
}
// Step 3: Notify Employee
POST /api/step-definitions
{
processDefinitionId: "cuid_abc123",
code: "NOTIFY_RESULT",
name: "Thông báo kết quả",
order: 3,
type: "NOTIFICATION",
assigneeType: "DYNAMIC",
assigneeConfig: {
"expression": "requester"
}
}
➡️ Step 4: PM nối các Steps bằng Transitions:
// POST /api/transition-rules (batch)
[
{ fromStepId: null, toStepId: "step1_id", triggerAction: "COMPLETE" }, // START → Manager
{ fromStepId: "step1_id", toStepId: "step2_id", triggerAction: "APPROVE" }, // Manager ✓ → HR
{ fromStepId: "step1_id", toStepId: null, triggerAction: "REJECT" }, // Manager ✗ → END (rejected)
{ fromStepId: "step2_id", toStepId: "step3_id", triggerAction: "APPROVE" }, // HR ✓ → Notify
{ fromStepId: "step2_id", toStepId: null, triggerAction: "REJECT" }, // HR ✗ → END (rejected)
{ fromStepId: "step3_id", toStepId: null, triggerAction: "COMPLETE" } // Notify → END (completed)
]
🔔 Step 5: PM cấu hình Notifications:
// POST /api/notification-templates
{
processDefinitionId: "cuid_abc123",
code: "STEP_ASSIGNED_MANAGER",
triggerEvent: "STEP_ASSIGNED",
triggerStepCode: "MANAGER_APPROVAL",
channels: ["EMAIL", "TELEGRAM", "IN_APP"],
recipientType: "ASSIGNEE",
subjectTemplate: "🗓️ Đơn nghỉ phép #{{referenceNumber}} cần duyệt",
bodyTemplate: "Xin chào {{recipientName}},\n\n{{requesterName}} đã gửi đơn xin nghỉ phép:\n- Loại: {{data.leaveType}}\n- Từ: {{data.startDate}} đến {{data.endDate}}\n- Lý do: {{data.reason}}\n\nVui lòng duyệt tại: {{approvalLink}}",
telegramTemplate: "📋 Đơn nghỉ #{{referenceNumber}} từ {{requesterName}}. Duyệt: {{approvalLink}}"
}
✅ Step 6: PM click "Publish" → isActive: true → Workflow sẵn sàng sử dụng!
FLOW 2: Nhân viên Tạo Đơn (Process Instance)
📝 Step 1: Nhân viên mở trang "Tạo đơn mới", chọn loại quy trình:
// GET /api/process-definitions?isActive=true&category=HR
// Response: list of available process types
[
{ id: "cuid_abc123", code: "LEAVE_REQUEST", name: "Đơn xin nghỉ phép", icon: "calendar" },
{ id: "cuid_def456", code: "EXPENSE_CLAIM", name: "Đơn thanh toán chi phí", icon: "receipt" }
]
📋 Step 2: Nhân viên click "Đơn xin nghỉ phép" → FE fetch formSchema → render dynamic form:
// GET /api/process-definitions/cuid_abc123
// Response includes formSchema → FE renders:
// - Select: Loại nghỉ phép (ANNUAL, SICK, UNPAID...)
// - DatePicker: Ngày bắt đầu
// - DatePicker: Ngày kết thúc
// - Textarea: Lý do
// - FileUpload: Đính kèm
💾 Step 3: Nhân viên điền form và click "Lưu nháp":
// POST /api/process-instances
{
processDefinitionId: "cuid_abc123",
data: {
leaveType: "ANNUAL",
startDate: "2026-06-01",
endDate: "2026-06-03",
reason: "Nghỉ phép năm, đi du lịch với gia đình",
attachments: []
}
}
// Response: ProcessInstance created
{
id: "inst_xyz789",
referenceNumber: "LV-2026-0042", // auto-generated
status: "DRAFT",
requesterId: "employee_user_id",
data: { ... },
createdAt: "2026-05-22T10:30:00Z"
}
🚀 Step 4: Nhân viên click "Gửi đơn" → Workflow Engine bắt đầu:
// POST /api/process-instances/inst_xyz789/submit
// Backend WorkflowEngine.submitProcess() does:
// 1. Validate form data against formSchema (Zod)
// 2. Find first step (MANAGER_APPROVAL)
// 3. Resolve assignee (requester's manager → "manager_user_id")
// 4. Create StepInstance
// 5. Update ProcessInstance status
// 6. Trigger notifications
// INSERT INTO StepInstance
{
id: "step_inst_001",
processInstanceId: "inst_xyz789",
stepDefinitionId: "step1_id",
assigneeId: "manager_user_id", // resolved from REQUESTER_MANAGER
status: "ASSIGNED",
assignedAt: "2026-05-22T10:31:00Z",
dueAt: "2026-05-24T10:31:00Z" // +48 hours
}
// UPDATE ProcessInstance
{
status: "SUBMITTED" → "IN_PROGRESS",
currentStepId: "step_inst_001",
submittedAt: "2026-05-22T10:31:00Z"
}
// INSERT INTO ProcessHistory
{
processInstanceId: "inst_xyz789",
actorId: "employee_user_id",
action: "SUBMITTED",
fromStatus: "DRAFT",
toStatus: "IN_PROGRESS"
}
📧 Step 5: Hệ thống gửi thông báo cho Manager:
// NotificationService triggered by STEP_ASSIGNED event
// Interpolate template với data:
Email to: manager@company.com
Subject: 🗓️ Đơn nghỉ phép #LV-2026-0042 cần duyệt
Body: Xin chào Nguyễn Văn A,
Trần Thị B đã gửi đơn xin nghỉ phép:
- Loại: Nghỉ phép năm
- Từ: 01/06/2026 đến 03/06/2026
- Lý do: Nghỉ phép năm, đi du lịch với gia đình
Vui lòng duyệt tại: https://erp.company.com/approvals/step_inst_001
// Telegram message also sent with inline button
FLOW 3: Manager Duyệt Đơn
👀 Step 1: Manager mở link từ email/Telegram → xem chi tiết đơn:
// GET /api/step-instances/step_inst_001
{
id: "step_inst_001",
status: "ASSIGNED",
stepDefinition: { code: "MANAGER_APPROVAL", name: "Quản lý duyệt", allowReject: true },
processInstance: {
referenceNumber: "LV-2026-0042",
requester: { name: "Trần Thị B", department: "Marketing" },
data: { leaveType: "ANNUAL", startDate: "2026-06-01", ... }
}
}
✅ Step 2: Manager click "Duyệt" với comment:
// POST /api/step-instances/step_inst_001/complete
{
decision: "APPROVED",
comment: "Đồng ý, chúc em đi chơi vui vẻ!"
}
// Backend WorkflowEngine.completeStep() does:
// 1. Validate assignee matches current user
// 2. Validate step status is ASSIGNED or IN_PROGRESS
// 3. Update StepInstance
// 4. Find next transition (APPROVE → HR_CONFIRM)
// 5. Create next StepInstance
// 6. Trigger notifications
// UPDATE StepInstance (current)
{
status: "COMPLETED",
decision: "APPROVED",
decisionComment: "Đồng ý, chúc em đi chơi vui vẻ!",
decisionAt: "2026-05-22T14:00:00Z",
completedAt: "2026-05-22T14:00:00Z"
}
// INSERT INTO StepInstance (next step)
{
id: "step_inst_002",
processInstanceId: "inst_xyz789",
stepDefinitionId: "step2_id", // HR_CONFIRM
assigneeId: "hr_staff_user_id", // resolved from ROLE
status: "ASSIGNED",
assignedAt: "2026-05-22T14:00:01Z",
dueAt: "2026-05-23T14:00:01Z" // +24 hours
}
// UPDATE ProcessInstance
{
currentStepId: "step_inst_002"
}
// INSERT INTO ProcessHistory
{
action: "STEP_COMPLETED",
stepCode: "MANAGER_APPROVAL",
comment: "Đồng ý, chúc em đi chơi vui vẻ!"
}
FLOW 4: Quy trình Hoàn thành
// Sau khi HR duyệt → Step NOTIFY_RESULT được tạo (type: NOTIFICATION)
// WorkflowEngine tự động complete notification step
// INSERT INTO StepInstance
{
id: "step_inst_003",
stepDefinitionId: "step3_id", // NOTIFY_RESULT
assigneeId: "employee_user_id", // requester
status: "COMPLETED", // auto-completed for NOTIFICATION type
completedAt: "2026-05-22T16:00:00Z"
}
// Transition COMPLETE → toStepId: null (END)
// WorkflowEngine.completeProcess()
// UPDATE ProcessInstance
{
status: "COMPLETED",
currentStepId: null,
completedAt: "2026-05-22T16:00:00Z"
}
// INSERT INTO ProcessHistory
{
action: "COMPLETED",
fromStatus: "IN_PROGRESS",
toStatus: "COMPLETED"
}
// Trigger PROCESS_COMPLETED notification to requester
// "🎉 Đơn nghỉ phép #LV-2026-0042 đã được duyệt!"
FLOW 5: Manager Từ chối Đơn (REJECT)
❌ Step 1: Manager click "Từ chối" với lý do:
// POST /api/step-instances/step_inst_001/complete
{
decision: "REJECTED",
comment: "Không đủ nhân sự trong thời gian này. Đề nghị chọn ngày khác."
}
// Backend WorkflowEngine.completeStep() does:
// 1. Update StepInstance với decision: REJECTED
// 2. Find transition với triggerAction: REJECT
// 3. Execute transition (toStepId: null = END)
💾 Step 2: Database Updates:
// UPDATE StepInstance
{
id: "step_inst_001",
status: "REJECTED", // ← not COMPLETED
decision: "REJECTED",
decisionComment: "Không đủ nhân sự trong thời gian này...",
decisionAt: "2026-05-22T15:00:00Z",
completedAt: "2026-05-22T15:00:00Z"
}
// Find TransitionRule
{
fromStepId: "step1_id", // MANAGER_APPROVAL
toStepId: null, // → END (no next step)
triggerAction: "REJECT"
}
// UPDATE ProcessInstance
{
status: "REJECTED", // ← Process ends with REJECTED status
currentStepId: null,
completedAt: "2026-05-22T15:00:00Z"
}
// INSERT INTO ProcessHistory
{
processInstanceId: "inst_xyz789",
actorId: "manager_user_id",
action: "STEP_REJECTED",
stepCode: "MANAGER_APPROVAL",
fromStatus: "IN_PROGRESS",
toStatus: "REJECTED",
comment: "Không đủ nhân sự trong thời gian này..."
}
📧 Step 3: Thông báo cho nhân viên:
// NotificationService triggered by STEP_REJECTED event
// Template: NotificationTemplate với triggerEvent: "STEP_REJECTED"
Email to: employee@company.com
Subject: ❌ Đơn nghỉ phép #LV-2026-0042 bị từ chối
Body: Xin chào Trần Thị B,
Đơn nghỉ phép của bạn đã bị từ chối bởi Nguyễn Văn A (Quản lý).
Lý do: Không đủ nhân sự trong thời gian này. Đề nghị chọn ngày khác.
Bạn có thể tạo đơn mới tại: https://erp.company.com/leave-request/new
// Telegram: "❌ Đơn #LV-2026-0042 bị từ chối. Lý do: ..."
🔄 Option: Cho phép tạo đơn mới (Resubmit):
// Nhân viên có thể:
// 1. Tạo đơn MỚI với ngày khác (recommended)
// 2. Hoặc nếu system cho phép REVISION:
// POST /api/process-instances/inst_xyz789/revise
{
data: {
leaveType: "ANNUAL",
startDate: "2026-06-15", // ← changed dates
endDate: "2026-06-17",
reason: "Nghỉ phép năm (đã đổi ngày theo góp ý của quản lý)"
}
}
// This creates NEW ProcessInstance linked to original
{
id: "inst_revision_001",
referenceNumber: "LV-2026-0042-R1", // revision suffix
previousVersionId: "inst_xyz789", // link to rejected version
status: "DRAFT"
}
FLOW 6: Timeout & Auto-Escalation
⏰ Background: Cron job check timeout mỗi 5 phút:
// TimeoutCheckerCronJob runs every 5 minutes
// SELECT * FROM StepInstance
// WHERE status IN ('ASSIGNED', 'IN_PROGRESS')
// AND dueAt < NOW()
// AND dueAt IS NOT NULL
// Found: step_inst_001 with dueAt: "2026-05-24T10:31:00Z"
// Current time: "2026-05-24T12:00:00Z" (past due)
// StepDefinition has:
{
timeoutHours: 48,
escalationRoleId: "hr_admin_role"
}
🔔 Step 1: Send Reminder (nếu chưa escalate):
// First timeout: send reminder to original assignee
// Check: Is this first timeout or repeat?
// If first timeout (within grace period, e.g., +4 hours):
// NotificationService sends REMINDER
Email to: manager@company.com
Subject: ⚠️ NHẮC NHỞ: Đơn #LV-2026-0042 cần duyệt GẤP
Body: Xin chào Nguyễn Văn A,
Đơn nghỉ phép #LV-2026-0042 đã QUÁ HẠN duyệt.
- Đã chờ: 50 giờ (hạn: 48 giờ)
- Nhân viên: Trần Thị B
- Ngày nghỉ: 01/06 - 03/06/2026
Vui lòng duyệt NGAY: https://erp.company.com/approvals/step_inst_001
Nếu không có phản hồi trong 4 giờ, đơn sẽ được chuyển lên HR Admin.
// INSERT ProcessHistory
{
action: "STEP_TIMEOUT",
stepCode: "MANAGER_APPROVAL",
metadata: { reminderSent: true, reminderAt: "2026-05-24T12:00:00Z" }
}
⬆️ Step 2: Auto-Escalate (nếu vẫn không response):
// After grace period (e.g., +4 hours), still no action
// TimeoutCheckerCronJob escalates
// Option A: Reassign to escalation role
// UPDATE StepInstance
{
id: "step_inst_001",
assigneeId: "hr_admin_user_id", // ← reassigned from manager
status: "ASSIGNED", // ← reset to ASSIGNED
assignedAt: "2026-05-24T16:00:00Z",
dueAt: "2026-05-25T16:00:00Z", // ← new deadline (+24h)
metadata: {
escalatedFrom: "manager_user_id",
escalatedAt: "2026-05-24T16:00:00Z",
escalationReason: "TIMEOUT"
}
}
// INSERT ProcessHistory
{
action: "STEP_ESCALATED",
stepCode: "MANAGER_APPROVAL",
metadata: {
fromAssignee: "manager_user_id",
toAssignee: "hr_admin_user_id",
reason: "TIMEOUT after 52 hours"
}
}
// Notify original assignee (manager)
Email: "⚠️ Đơn #LV-2026-0042 đã được chuyển lên HR Admin do quá hạn."
// Notify new assignee (HR Admin)
Email: "📋 Đơn #LV-2026-0042 cần xử lý GẤP (escalated từ Nguyễn Văn A)"
🚫 Option B: Auto-Reject (nếu config):
// Alternative: StepDefinition có timeoutAction: "AUTO_REJECT"
// Instead of escalating, system auto-rejects
// Find TransitionRule
{
fromStepId: "step1_id",
toStepId: null, // → END
triggerAction: "TIMEOUT" // ← special trigger
}
// UPDATE StepInstance
{
status: "TIMEOUT",
decision: null, // no human decision
decisionComment: "Tự động từ chối do quá hạn duyệt",
completedAt: "2026-05-24T16:00:00Z"
}
// UPDATE ProcessInstance
{
status: "CANCELLED", // or "TIMEOUT"
completedAt: "2026-05-24T16:00:00Z"
}
// Notify requester
Email: "⏰ Đơn #LV-2026-0042 đã hết hạn do không được duyệt trong 48h.
Vui lòng tạo đơn mới."
📊 Timeout Configuration Summary:
| Config | Field | Example |
| Timeout duration | StepDefinition.timeoutHours | 48 (hours) |
| Grace period | StepDefinition.gracePeriodHours | 4 (hours before escalate) |
| Escalation target | StepDefinition.escalationRoleId | FK to Role table |
| Timeout action | StepDefinition.timeoutAction | ESCALATE | AUTO_REJECT | REMIND_ONLY |
| Max escalations | StepDefinition.maxEscalations | 2 (then auto-reject) |
FLOW 7: Delegation (Ủy quyền)
🔄 Use case: Manager đi công tác, delegate cho Deputy:
// POST /api/step-instances/step_inst_001/delegate
{
delegateToUserId: "deputy_manager_id",
reason: "Đi công tác đến 25/05, ủy quyền cho Phó phòng"
}
// Validate: StepDefinition.allowDelegate == true
// UPDATE StepInstance
{
assigneeId: "deputy_manager_id", // ← new assignee
delegatedFromId: "manager_user_id", // ← track original
status: "ASSIGNED",
assignedAt: "2026-05-22T11:00:00Z" // ← reset assignment time
}
// INSERT ProcessHistory
{
action: "STEP_DELEGATED",
stepCode: "MANAGER_APPROVAL",
actorId: "manager_user_id",
metadata: {
delegatedTo: "deputy_manager_id",
reason: "Đi công tác đến 25/05..."
}
}
// Notify deputy
Email: "📋 Nguyễn Văn A đã ủy quyền đơn #LV-2026-0042 cho bạn xử lý."
// Notify requester (optional)
Email: "📌 Đơn của bạn đã được chuyển từ Nguyễn Văn A sang Trần Văn C."
📊 Visual Flow Summary:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ DRAFT │────▶│ SUBMITTED │────▶│ IN_PROGRESS │────▶│ COMPLETED │
│ │ │ │ │ │ │ │
│ Employee │ │ Create │ │ Steps │ │ All steps │
│ fills form │ │ Step 1 │ │ executing │ │ done │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ StepInstance│ │ StepInstance│
│ MANAGER │────▶│ HR_CONFIRM │────▶ ... ────▶ END
│ APPROVAL │ │ │
│ assignee: │ │ assignee: │
│ manager_id │ │ hr_staff_id │
└─────────────┘ └─────────────┘
│
▼ (if REJECT)
┌─────────────┐
│ REJECTED │
│ │
│ Process ends│
│ with reject │
└─────────────┘