Appearance
Workflow Engine
Overview
Workflow Engine (src/modules/workflow/) triển khai hệ thống workflow phê duyệt trực quan. Hệ thống hỗ trợ:
- Workflow Definitions với các step và transition
- Instances — các lần thực thi đang chạy, gắn với một item cụ thể trong collection
- Task Inbox — danh sách task đang chờ xử lý được gán cho người dùng
- Audit Log — lịch sử đầy đủ của mọi hành động
Core Concepts
Trigger Events
| Event | Mô tả |
|---|---|
items.create | Tự động kích hoạt khi một item được tạo trong collection đã liên kết |
items.update | Tự động kích hoạt khi item được cập nhật (có thể kèm filter) |
versions.submit | Tự động kích hoạt khi một version được gửi để xét duyệt (xem Content Moderation) |
manual | Kích hoạt thủ công qua POST /workflow-instances |
Step Types
| Loại | Mô tả |
|---|---|
start | Điểm bắt đầu của workflow |
approval | Yêu cầu phê duyệt từ con người với danh sách người được gán có thể cấu hình |
condition | Định tuyến luồng dựa trên một biểu thức điều kiện |
action | Thực thi một hành động tự động (ví dụ: cập nhật item, gửi thông báo) |
notification | Gửi thông báo mà không chờ phê duyệt |
end | Step kết thúc (hoàn tất instance) |
Only `start`, `approval`, `action`, `end` are fully driven by the engine
activateStep auto-advances start, action, and end, and parks on approval to wait for a human. There is no auto-advance branch for condition or notification steps — if placed as a node, they activate, find no assignee, and stall. So:
- Branching is done with a transition of
trigger: "condition"out of a real step (usuallystart), not with aconditionnode. See Branching. - Notifications are sent with an
actionstep (action_type: "send_notification"), not anotificationnode.
Assignment Types (for Approval steps)
| Loại | Mô tả |
|---|---|
role | Gán cho tất cả người dùng có một role cụ thể |
user | Gán cho một người dùng cụ thể theo UUID |
field | Đọc người được gán từ một field trên item (dùng effective item = main + version delta, giống conditions) |
creator | Gán cho người dùng đã khởi động instance |
auto | Tự động phê duyệt (dùng kèm với timeout action auto_approve) |
Approval Modes
| Chế độ | Mô tả |
|---|---|
any | Bất kỳ một người được gán nào phê duyệt là đủ |
all | Tất cả người được gán đều phải phê duyệt |
majority | Hơn một nửa phải phê duyệt |
count | Một số lượng cụ thể (được đặt trong approval_count) |
Approval Outcome & Version Promotion
Đây là phần dễ gây hiểu lầm nhất của engine, vì vậy hãy đọc kỹ khi thiết lập flow phê duyệt nội dung (workflow với trigger_event = versions.submit).
The outcome is decided by the transition trigger, not by the step
Khi luồng đến bước end, engine phân nhánh dựa trên cách luồng đến đó — trigger của transition dẫn vào bước end — không phải dựa trên bản thân bước end:
trigger === 'approve' → instance 'completed' + promote version (merge delta into the main item)
trigger === 'reject' → instance 'rejected' + version 'rejected' (NOT promoted)Promotion chỉ chạy khi trigger === 'approve' và instance có version_id, và nó chạy bên trong transaction phê duyệt — xem Execution Model.
You only need ONE end step
Cả transition approve và reject đều có thể trỏ đến cùng một node end — engine vẫn cho ra kết quả đúng vì nó dựa vào trigger. Bước end không mang logic riêng.
Cấu trúc phê duyệt nội dung tối giản:
start ──always──▶ approval ──approve──▶ end
└───────reject───▶ end (same node)| Từ | Trigger | Đến | Kết quả |
|---|---|---|---|
approval | approve | end | instance completed, version được promote vào item chính |
approval | reject | end (cùng node) | instance rejected, version được đánh dấu rejected, không promote |
Reject đến
endlà an toàn — không bao giờ promote, vì promotion bị chặn bởi điều kiệntrigger === 'approve'. Template thông báo cũng theo trigger:workflow_completedkhi approve,workflow_task_rejectedkhi reject.
When to split into two end steps (optional)
Chỉ dùng hai node end riêng biệt khi bạn muốn hành vi khác nhau sau mỗi kết quả — ví dụ nhánh reject chạy thêm một bước action/notification trước, hoặc bạn muốn audit/UI phân biệt rõ "End – Approved" và "End – Rejected". Điều này không bắt buộc để hệ thống hoạt động đúng.
The one thing that breaks it
Transition reject phải mang trigger: "reject". Nếu một nhánh reject bị nối nhầm với trigger: "approve" (hoặc always) vào bước end, nó sẽ promote khi reject. Các trigger phải khớp với hành động tương ứng.
Explicit promotion mid-flow
Ngoài tính năng tự động promote ở bước end, một bước action với action_type = "promote_version" có thể promote version một cách tường minh tại bất kỳ điểm nào trong luồng. Hầu hết các flow phê duyệt không cần điều này — để transition approve → end xử lý là đủ.
Branching with conditions
Để định tuyến luồng theo các nhánh khác nhau, thêm nhiều transition trigger: "condition" từ một bước (bước start hoạt động tốt cho việc định tuyến nội dung). Bộ matching transition hoạt động theo hai lượt:
- Lượt 1 — khớp transition có
triggerbằng trigger advance (approve/reject) hoặcalways, theo thứ tựsort_order. - Lượt 2 (chỉ khi lượt 1 không tìm thấy gì) — khớp transition
trigger: "condition"đầu tiên cóconditionđánh giá là true, theo thứ tựsort_order.
Các transition `condition` chỉ kích hoạt nếu KHÔNG có transition `always`/`approve` nào khớp trước
Nếu bạn đặt một transition always bên cạnh các transition condition, always sẽ thắng ở lượt 1 và các điều kiện không bao giờ được đánh giá. Để phân nhánh, chỉ dùng transition condition trên bước đó, và đặt transition cuối cùng làm catch-all (một điều kiện luôn đúng, ví dụ { "<field>": { "_nnull": true } }) với thứ tự cuối.
Conditions đánh giá trên "effective item" = main item + version delta
Với instance có version_id (luồng versions.submit), conditions được đánh giá trên trạng thái người dùng đã submit: hàng gốc trong collection merge với delta của version đang chờ duyệt, trong đó delta (payload user) thắng khi trùng field. Nhờ vậy, field chỉ được set trong version (ví dụ primary_category) vẫn rẽ nhánh đúng — không bị đọc giá trị cũ của base. Hành vi này nhất quán ở cả 3 nơi: trigger_filter (lúc auto-start), transition condition (engine), và field assignee (resolver) — qua helper dùng chung getEffectiveItem().
Hệ quả thiết kế: giá trị do chính flow ghi (ví dụ một bước action update_field) không bao giờ đè field mà user đã submit → rẽ nhánh/phân công luôn theo submission, không theo side-effect của flow. (Riêng field chỉ do flow ghi mà user không submit thì vẫn lấy từ base.)
Condition operators: hỗ trợ nhóm logic _and / _or (lồng nhau, đúng định dạng UI condition builder sinh ra) và các operator: _eq, _neq, _lt, _lte, _gt, _gte, _in, _nin, _null, _nnull, _contains, _ncontains/_icontains, _starts_with/_ends_with (+ biến thể _n*/_i*), _between/_nbetween, _empty/_nempty, _regex. So sánh _eq/_neq/_in/_nin dùng loose compare (String(a) === String(b)) nên relation PK chọn từ UI (chuỗi "3") khớp giá trị int 3; null chỉ bằng null.
Một bộ matcher in-memory duy nhất
Việc đánh giá filter in-memory (workflow conditions, trigger_filter, permission payload validation, registration email filter) dùng chung service/api/src/utils/match-filter.ts (matchesFilter), giữ parity operator với AST query engine bằng test tự động (test/unit/utils/match-filter.test.ts) — thêm operator ở AST mà matcher thiếu sẽ làm CI fail.
Action steps (action_config)
Bước action chạy theo action_type, đọc cấu hình từ action_config (JSON):
action_type | action_config | Ghi chú |
|---|---|---|
update_field | { "field": "status", "value": "x" } hoặc nhiều field: { "fields": { "status": "x", "reviewed": true } } | Cập nhật item của instance. Hỗ trợ cả 2 shape (cũ 1 field + mới nhiều field), gộp lại update 1 lần |
send_notification | payload cho notify.trigger (template_id, recipients, ...) | Phát event notify, không chờ |
promote_version | — | Promote version tường minh giữa luồng (xem [[versions]]) |
`update_field` ghi bằng raw query — có chủ đích
update_field ghi thẳng qua Knex (không qua ItemsService), nên không emit items.update → không tạo revision/activity và không tự trigger lại workflow (tránh vòng lặp vô hạn). Đổi lại: chỉ đúng với scalar column; field quan hệ/special (hash, json cần transform) sẽ lưu sai. Nếu cần xử lý payload đầy đủ, chuyển sang ItemsService.updateOne(..., { emitEvents: false }) (giữ chống-loop) — nhưng cân nhắc kỹ vì sẽ khác hành vi hiện tại.
Execution Model (engine internals)
Hiểu phần này rất quan trọng khi suy luận về timing, lỗi và version promotion.
- Hoàn toàn đồng bộ từ góc độ của caller.
approveStep/rejectStep/startInstance/handleTimeoutawait toàn bộ chuỗi — advance step hiện tại → activate step tiếp theo → chạy actions → đếnend→ promote version → phát thông báo. Khi lời gọi trả về, instance đã đạt trạng thái kết quả. (Các phiên bản trước trì hoãn công việc này bằngsetImmediate, khiến hành vi bị race condition và test không ổn định; cách đó đã bị loại bỏ.) - Cách tránh nested transaction. Mỗi lần kích hoạt step chạy trong transaction riêng của nó. Transaction trả về một descriptor nhỏ mô tả "cần làm gì tiếp theo" (step tiếp theo cần activate, thông báo cần gửi); caller xử lý nó trong một vòng lặp tuần tự bên ngoài transaction. Như vậy các transaction không bao giờ lồng nhau, nhưng mọi thứ đều được await.
- Version promotion chạy bên trong transaction phê duyệt. Khi phê duyệt cuối cùng của workflow có version,
VersionsService.promotechạy trong cùng transaction với lần phê duyệt đó. Nếu promotion thất bại, toàn bộ lần phê duyệt bị rollback: instance không được đánh dấucompletedvà version vẫn ở trạng thái chưa được phê duyệt — không có trạng thái "workflow completed nhưng version chưa được promote". Lời gọi API trả về lỗi để client có thể thử lại. Reject không bao giờ promote. - Notifications được phát ra sau khi transaction commit và được await; lỗi thông báo được ghi log nhưng không bao giờ chặn hoặc rollback workflow.
- Instance lỗi sẽ giải phóng version. Khi advance không tìm được transition khớp (hoặc step kế không tồn tại), instance chuyển
errorvà version được reset vềdraft(failInstanceInTrx). Không reset thì version kẹt ởpending(vìsubmit()chỉ nhậndraft/rejectedvà instanceerrorlà terminal) → editor không resubmit được. Sau khi release, submit lại sẽ tạo instance mới (instanceerrorcũ giữ làm lịch sử; chỉ instancerejectedmới được reactivate).
Xem [[versions]] → POST /versions/:id/promote để biết promotion ghi gì (create-or-update, relations được giữ nguyên, không có placeholder item).
Approval Outcome & Version Promotion
This is the most-misunderstood part of the engine, so read it carefully when wiring a content approval flow (a workflow with trigger_event = versions.submit).
The outcome is decided by the transition trigger, not by the step
When the flow reaches an end step, the engine branches on how it got there — the trigger of the transition that led into the end step — not on the end step itself:
trigger === 'approve' → instance status 'completed' + promoteVersion() (merge version delta into the main item)
trigger === 'reject' → instance status 'rejected' + version status 'rejected' (NOT promoted)Source: src/modules/workflow/engine.ts (advanceFromTrx, the nextStep.type === 'end' branch ~line 505–556, and completeInstance ~line 572). Promotion runs VersionsService.promote() only when trigger === 'approve' and the instance has a version_id.
You only need ONE end step
Both the approve and the reject transition can point to the same end node — the engine still produces the correct result because it keys off the trigger. The end step carries no logic of its own.
Minimal content-approval shape:
start ──always──▶ approval ──approve──▶ end
└───────reject───▶ end (same node)| From | Trigger | To | Result |
|---|---|---|---|
approval | approve | end | instance completed, version promoted into the main item |
approval | reject | end (same node) | instance rejected, version marked rejected, not promoted |
Reject reaching
endis safe — it never promotes, because promotion is gated ontrigger === 'approve'. The notification template also follows the trigger:workflow_completedon approve,workflow_task_rejectedon reject.
When to split into two end steps (optional)
Use separate end nodes only when you want different behaviour after each outcome — e.g. the reject branch runs an extra action/notification step first, or you want the audit/UI to clearly distinguish "End – Approved" vs "End – Rejected". It is not required for correctness.
The one thing that breaks it
The reject transition must carry trigger: "reject". If a reject path is mis-wired with trigger: "approve" (or always) into an end step, it will promote on reject. Triggers must match the action.
Explicit promotion mid-flow
Besides the automatic end-step promotion, an action step with action_type = "promote_version" promotes the version explicitly at any point in the flow (engine.ts executeAction). Most approval flows don't need this — letting the approve → end transition handle it is enough.
Data Model
odp_workflows
| Cột | Kiểu | Mô tả |
|---|---|---|
id | UUID | Khóa chính |
name | varchar | Tên hiển thị |
description | text | Mô tả |
collection | varchar | Collection liên kết (nullable với workflow thủ công) |
status | varchar | draft, active, archived |
trigger_event | varchar | items.create, items.update, versions.submit, manual |
trigger_filter | json | Điều kiện filter cho auto-trigger |
options | json | Cấu hình bổ sung |
created_by | UUID | UUID người tạo |
created_at | timestamp | |
updated_at | timestamp |
odp_workflow_steps
| Cột | Kiểu | Mô tả |
|---|---|---|
id | UUID | Khóa chính |
workflow_id | UUID | FK đến odp_workflows.id |
key | varchar | Khóa step duy nhất trong workflow |
name | varchar | Tên hiển thị |
type | varchar | Loại step |
assign_type | varchar | Phương thức gán |
assign_value | varchar | UUID role, UUID người dùng, hoặc tên field |
approval_mode | varchar | Chế độ phê duyệt |
approval_count | integer | Số lượt phê duyệt yêu cầu cho chế độ count |
timeout_minutes | integer | Thời gian timeout tính bằng phút (null = không có timeout) |
timeout_action | varchar | escalate, auto_approve, auto_reject, notify |
escalate_to | varchar | UUID người dùng để leo thang |
action_type | varchar | Dùng cho bước action |
action_config | json | Cấu hình hành động |
position_x | integer | Vị trí trên canvas trực quan |
position_y | integer | Vị trí trên canvas trực quan |
sort_order | integer | Thứ tự step |
odp_workflow_transitions
| Cột | Kiểu | Mô tả |
|---|---|---|
id | UUID | Khóa chính |
workflow_id | UUID | FK đến workflow |
from_step_id | UUID | Step nguồn |
to_step_id | UUID | Step đích |
trigger | varchar | approve, reject, timeout, condition, always |
condition | json | Biểu thức điều kiện (cho trigger condition) |
sort_order | integer | Độ ưu tiên khi nhiều transition khớp |
label | varchar | Nhãn hiển thị trên cạnh của canvas |
odp_workflow_instances
| Cột | Kiểu | Mô tả |
|---|---|---|
id | UUID | Khóa chính |
workflow_id | UUID | FK đến workflow definition |
collection | varchar | Collection mà item thuộc về |
item_id | varchar | Khóa chính của item liên kết |
version_id | varchar | Version đang được xét duyệt (nullable — null với workflow không có version, xem Content Moderation) |
status | varchar | running, completed, rejected, cancelled, error |
current_step_id | UUID | Step đang active (nullable khi đã hoàn tất) |
started_by | UUID | Người dùng đã kích hoạt instance |
started_at | timestamp | |
completed_at | timestamp |
odp_workflow_instance_steps
| Cột | Kiểu | Mô tả |
|---|---|---|
id | UUID | Khóa chính |
instance_id | UUID | FK đến instance |
step_id | UUID | FK đến workflow step |
status | varchar | pending, active, approved, rejected, skipped, timed_out |
approvals | json | Mảng {user_id, action, comment, timestamp} |
activated_at | timestamp | Thời điểm step này trở thành active |
completed_at | timestamp | Thời điểm step này được giải quyết |
timeout_at | timestamp | Thời hạn timeout |
odp_workflow_actions
| Cột | Kiểu | Mô tả |
|---|---|---|
id | UUID | Khóa chính |
instance_id | UUID | FK đến instance |
step_id | UUID | FK đến step (nullable cho các hành động ở cấp instance) |
user_id | UUID | Người dùng thực hiện hành động |
action | varchar | start, approve, reject, delegate, comment, escalate, cancel, auto_approve, timeout |
comment | text | Bình luận tùy chọn |
data | json | Dữ liệu hành động bổ sung |
created_at | timestamp |
Workflow Definition Endpoints
POST /workflows
Tạo một workflow definition. Chỉ Admin + quyền workflow.manage.
Request Body
json
{
"name": "Article Approval",
"description": "Review and approve articles before publishing",
"collection": "articles",
"status": "draft",
"trigger_event": "manual",
"trigger_filter": null
}Response 201
json
{ "data": { "id": "workflow-uuid" } }GET /workflows
Lấy danh sách workflow definition.
Yêu cầu xác thực: quyền workflow.view
Query Parameters
| Tham số | Mô tả |
|---|---|
status | Lọc theo status |
collection | Lọc theo collection |
limit | Số kết quả tối đa |
offset | Phân trang |
GET /workflows/:id
Đọc một workflow definition (bao gồm steps và transitions).
Response 200
json
{
"data": {
"id": "workflow-uuid",
"name": "Article Approval",
"status": "active",
"steps": [
{
"id": "step-uuid",
"key": "review",
"name": "Editor Review",
"type": "approval",
"assign_type": "role",
"assign_value": "editor-role-uuid",
"approval_mode": "any"
}
],
"transitions": [
{
"id": "trans-uuid",
"from_step_id": "start-step-uuid",
"to_step_id": "step-uuid",
"trigger": "always"
}
]
}
}PATCH /workflows/:id
Cập nhật một workflow definition. Chỉ Admin.
DELETE /workflows/:id
Xóa một workflow. Chỉ Admin. Không thể xóa workflow đang có instance đang chạy.
POST /workflows/:id/duplicate
Nhân bản một workflow (tạo bản draft mới). Chỉ Admin.
Response 201 — { "data": { "id": "new-workflow-uuid" } }
POST /workflows/:id/activate
Kích hoạt một workflow (status draft → active). Chỉ Admin.
Response 200 — { "data": { "id": "...", "status": "active" } }
POST /workflows/:id/deactivate
Vô hiệu hóa một workflow (status active → draft). Chỉ Admin.
Step Endpoints
POST /workflows/:id/steps
Thêm một step vào workflow. Chỉ Admin.
Request Body
json
{
"key": "manager_approval",
"name": "Manager Approval",
"type": "approval",
"assign_type": "user",
"assign_value": "manager-user-uuid",
"approval_mode": "any",
"timeout_minutes": 1440,
"timeout_action": "escalate",
"escalate_to": "director-user-uuid",
"position_x": 300,
"position_y": 200
}Response 201 — { "data": { "id": "step-uuid" } }
PATCH /workflows/:id/steps/:key
Cập nhật một step.
DELETE /workflows/:id/steps/:key
Xóa một step.
Transition Endpoints
POST /workflows/:id/transitions
Thêm một transition giữa các step. Chỉ Admin.
Request Body
json
{
"from_step_id": "review-step-uuid",
"to_step_id": "approval-step-uuid",
"trigger": "approve",
"label": "Approved by editor",
"condition": null
}Response 201 — { "data": { "id": "trans-uuid" } }
PATCH /workflows/:id/transitions/:tid
Cập nhật một transition.
DELETE /workflows/:id/transitions/:tid
Xóa một transition.
Instance Endpoints
POST /workflow-instances
Khởi động một workflow instance cho một item.
Yêu cầu xác thực: quyền workflow.start trên collection
Request Body
json
{
"workflow_id": "workflow-uuid",
"collection": "articles",
"item_id": "article-uuid"
}Response 201
json
{
"data": {
"id": "instance-uuid",
"workflow_id": "workflow-uuid",
"collection": "articles",
"item_id": "article-uuid",
"status": "running",
"current_step_id": "start-step-uuid",
"started_at": "2026-03-26T10:00:00.000Z"
}
}GET /workflow-instances
Lấy danh sách instance.
Yêu cầu xác thực: quyền workflow.view
Query Parameters
| Tham số | Mô tả |
|---|---|
workflow_id | Lọc theo workflow |
collection | Lọc theo collection |
item_id | Lọc theo item |
status | Lọc theo status |
limit / offset | Phân trang |
GET /workflow-instances/:id
Đọc một instance (bao gồm trạng thái step, actions, workflow steps).
Yêu cầu xác thực: workflow.view HOẶC workflow.participate
POST /workflow-instances/:id/cancel
Hủy một instance đang chạy. Chỉ Admin.
Request Body
json
{
"comment": "Cancelling due to outdated content"
}Step Action Endpoints
Các endpoint này cho phép những người tham gia workflow thực hiện hành động trên các step đang chờ.
POST /workflow-instances/:id/steps/:stepId/approve
Phê duyệt step hiện tại.
Yêu cầu xác thực: quyền workflow.participate trên collection
Request Body
json
{
"comment": "Looks good, approved."
}Response 200 — { "data": { "success": true } }
Sau khi phê duyệt, engine đánh giá các transition để tự động chuyển sang step tiếp theo.
POST /workflow-instances/:id/steps/:stepId/reject
Từ chối step hiện tại.
Yêu cầu xác thực: quyền workflow.participate
Request Body
json
{
"comment": "Content needs revision."
}Bình luận khi từ chối là bắt buộc.
POST /workflow-instances/:id/steps/:stepId/delegate
Ủy quyền task cho người dùng khác.
Yêu cầu xác thực: quyền workflow.participate
Request Body
json
{
"to_user": "colleague-user-uuid",
"comment": "Delegating to editor"
}Người dùng đích cũng phải có quyền workflow.participate.
POST /workflow-instances/:id/steps/:stepId/comment
Thêm bình luận mà không thực hiện hành động.
Yêu cầu xác thực: quyền workflow.participate
Request Body
json
{
"comment": "Please review section 3 carefully."
}Task Inbox Endpoints
GET /workflow-tasks
Lấy danh sách task đang chờ của người dùng hiện tại (hộp thư đến cá nhân).
Yêu cầu xác thực: quyền workflow.participate
Query Parameters
| Tham số | Mô tả |
|---|---|
limit | Số kết quả tối đa |
offset | Phân trang |
Response 200
json
{
"data": [
{
"id": "instance-step-uuid",
"step_id": "step-uuid",
"instance_id": "instance-uuid",
"workflow_name": "Article Approval",
"step_name": "Editor Review",
"collection": "articles",
"item_id": "article-uuid",
"status": "active",
"activated_at": "2026-03-26T10:00:00.000Z",
"timeout_at": "2026-03-27T10:00:00.000Z",
"actions_available": ["approve", "reject", "delegate", "comment"]
}
],
"meta": { "total": 5 }
}Lưu ý: Dùng step_id (không phải id) khi gọi các endpoint approve/reject/delegate.
GET /workflow-tasks/count
Lấy số lượng task đang chờ của người dùng hiện tại (dùng để hiển thị badge).
Response 200
json
{
"data": { "count": 3 }
}GET /workflow-tasks/all
Lấy tất cả task đang active trên mọi người dùng. Chỉ Admin.
Audit Log Endpoints
GET /workflow-instances/:id/actions
Lấy toàn bộ lịch sử hành động của một workflow instance.
Yêu cầu xác thực: workflow.view HOẶC workflow.participate
Response 200
json
{
"data": [
{
"id": "action-uuid",
"instance_id": "instance-uuid",
"step_id": "step-uuid",
"user_id": "user-uuid",
"action": "approve",
"comment": "Looks great!",
"created_at": "2026-03-26T11:00:00.000Z"
}
]
}GET /workflow-actions
Lấy tất cả actions trên mọi instance. Chỉ Admin + workflow.manage.
Query Parameters
| Tham số | Mô tả |
|---|---|
instance_id | Lọc theo instance |
action | Lọc theo loại hành động |
limit / offset | Phân trang |
App Permissions
Các endpoint workflow kiểm tra quyền cấp ứng dụng lưu trong odp_app_permissions:
| Hành động | Đối tượng | Mô tả |
|---|---|---|
workflow.view | Người dùng chỉ xem | Đọc workflows và instances |
workflow.start | Người đóng góp | Khởi động instance mới |
workflow.participate | Người xét duyệt | Approve/reject/delegate tasks |
workflow.manage | Quản lý | Tạo/sửa/xóa workflow definition |
Tắt kiểm tra quyền ứng dụng bằng cách đặt ENABLE_APP_PERMISSIONS=false.
Scheduled Features
Bộ lên lịch workflow (registerWorkflowScheduler()) chạy định kỳ để:
- Phát hiện step đã timeout — Các step có
timeout_at < NOWđược xử lý theotimeout_action:auto_approve— Tự động phê duyệt stepauto_reject— Tự động từ chốiescalate— Gán lại choescalate_tomột lần, rồi xóatimeout_at(null) để không bị timeout lại. Escalate là one-shot (model chỉ có một cấpescalate_to); muốn nhắc lặp lại thì dùngnotify.notify— Gửiworkflow_remindercho người được gán và re-armtimeout_atcho chu kỳ nhắc tiếp theo (đây là cơ chế "nhắc lại" lặp lại có chủ đích).
Escalate không lặp vô hạn
Trước đây escalate re-arm timeout_at sau mỗi lần → scheduler escalate lại cùng step ở mỗi chu kỳ timeout_minutes, không bao giờ dừng ("escalate storm"): log + notify phình mãi, và DB contention (SQLite locked) nuốt cả email escalate. Nay escalate chạy một lần rồi dừng; cần nhắc lặp thì cấu hình timeout_action='notify'.
- Advance instances — Sau một timeout action, engine đánh giá lại các transition.
Example: Full Workflow Setup
bash
# 1. Create workflow definition
POST /workflows
{
"name": "Article Approval",
"collection": "articles",
"status": "draft",
"trigger_event": "manual"
}
# Returns: { "data": { "id": "wf-uuid" } }
# 2. Add steps
POST /workflows/wf-uuid/steps
{ "key": "start", "name": "Start", "type": "start", "position_x": 0, "position_y": 0 }
POST /workflows/wf-uuid/steps
{
"key": "review", "name": "Editor Review", "type": "approval",
"assign_type": "role", "assign_value": "editor-role-uuid",
"approval_mode": "any", "timeout_minutes": 1440,
"timeout_action": "auto_reject", "position_x": 200, "position_y": 0
}
POST /workflows/wf-uuid/steps
{ "key": "end", "name": "Done", "type": "end", "position_x": 400, "position_y": 0 }
# 3. Add transitions
POST /workflows/wf-uuid/transitions
{ "from_step_id": "start-uuid", "to_step_id": "review-uuid", "trigger": "always" }
POST /workflows/wf-uuid/transitions
{ "from_step_id": "review-uuid", "to_step_id": "end-uuid", "trigger": "approve" }
# 4. Activate
POST /workflows/wf-uuid/activate
# 5. Start an instance
POST /workflow-instances
{ "workflow_id": "wf-uuid", "collection": "articles", "item_id": "article-123" }
# 6. User approves their task
POST /workflow-instances/inst-uuid/steps/review-uuid/approve
{ "comment": "Article looks great!" }Example: Content Moderation Workflow (full-feature)
Quy trình xét duyệt hai cấp cho collection articles, tự động khởi động khi biên tập viên gửi version để xét duyệt. Ví dụ này khai thác hầu hết các tính năng của engine: trigger versions.submit, phân nhánh có điều kiện, ba chế độ approval, gán theo role, tất cả ba timeout action, bước action tự động update_field, và tự động promote khi approve.
versions.submit (articles)
│
[start] ──cond: category _in [news,legal] (sort 1)──▶ [legal_review]
└─cond: catch-all (sort 2)──────▶ [editor_review]
[legal_review] approval · role=Legal · mode=all · timeout 1440m → escalate→Chief
├─approve──▶ [editor_review]
└─reject───▶ [end]
[editor_review] approval · role=Editor · mode=any · timeout 480m → notify (reminder)
├─approve──▶ [chief_review]
└─reject───▶ [end]
[chief_review] approval · role=Chief · mode=count(2) · timeout 720m → escalate→Admin
├─approve──▶ [mark_reviewed]
└─reject───▶ [end]
[mark_reviewed] action · update_field { moderation_state: "approved" }
└─always──▶ [end]
[end] approve-path → promote version into the article + notify
reject-path → version rejected + notify (single shared end node)1. Workflow
jsonc
POST /workflows
{
"name": "Article Content Moderation",
"collection": "articles",
"trigger_event": "versions.submit",
// Tùy chọn: chỉ chạy cho các bài viết không phải bản nháp nội bộ.
// trigger_filter MERGES item + version delta, nên CÓ THỂ thấy các giá trị đã chỉnh sửa.
"trigger_filter": { "visibility": { "_neq": "internal" } }
}
// → { "data": { "id": "<wf>" } }2. Steps (each returns { data: { id } } — keep the ids for transitions)
jsonc
POST /workflows/<wf>/steps
{ "key": "start", "name": "Start", "type": "start" }
POST /workflows/<wf>/steps
{ "key": "legal_review", "name": "Legal Review", "type": "approval",
"assign_type": "role", "assign_value": "<legal-role-uuid>",
"approval_mode": "all",
"timeout_minutes": 1440, "timeout_action": "escalate", "escalate_to": "<chief-role-uuid>" }
POST /workflows/<wf>/steps
{ "key": "editor_review", "name": "Editor Review", "type": "approval",
"assign_type": "role", "assign_value": "<editor-role-uuid>",
"approval_mode": "any",
"timeout_minutes": 480, "timeout_action": "notify" }
POST /workflows/<wf>/steps
{ "key": "chief_review", "name": "Chief Editor Review", "type": "approval",
"assign_type": "role", "assign_value": "<chief-role-uuid>",
"approval_mode": "count", "approval_count": 2,
"timeout_minutes": 720, "timeout_action": "escalate", "escalate_to": "<admin-role-uuid>" }
POST /workflows/<wf>/steps
{ "key": "mark_reviewed", "name": "Mark Reviewed", "type": "action",
"action_type": "update_field",
"action_config": { "field": "moderation_state", "value": "approved" } }
POST /workflows/<wf>/steps
{ "key": "end", "name": "Done", "type": "end" }3. Transitions (use the step ids returned above)
jsonc
// start → branch (condition only; last one is the catch-all)
POST /workflows/<wf>/transitions
{ "from_step_id": "<start>", "to_step_id": "<legal_review>",
"trigger": "condition", "condition": { "category": { "_in": ["news", "legal"] } }, "sort_order": 1 }
POST /workflows/<wf>/transitions
{ "from_step_id": "<start>", "to_step_id": "<editor_review>",
"trigger": "condition", "condition": { "category": { "_nnull": true } }, "sort_order": 2 }
// legal_review → editor_review (approve) / end (reject)
POST /workflows/<wf>/transitions
{ "from_step_id": "<legal_review>", "to_step_id": "<editor_review>", "trigger": "approve" }
POST /workflows/<wf>/transitions
{ "from_step_id": "<legal_review>", "to_step_id": "<end>", "trigger": "reject" }
// editor_review → chief_review (approve) / end (reject)
POST /workflows/<wf>/transitions
{ "from_step_id": "<editor_review>", "to_step_id": "<chief_review>", "trigger": "approve" }
POST /workflows/<wf>/transitions
{ "from_step_id": "<editor_review>", "to_step_id": "<end>", "trigger": "reject" }
// chief_review → mark_reviewed (approve) / end (reject)
POST /workflows/<wf>/transitions
{ "from_step_id": "<chief_review>", "to_step_id": "<mark_reviewed>", "trigger": "approve" }
POST /workflows/<wf>/transitions
{ "from_step_id": "<chief_review>", "to_step_id": "<end>", "trigger": "reject" }
// mark_reviewed → end (always)
POST /workflows/<wf>/transitions
{ "from_step_id": "<mark_reviewed>", "to_step_id": "<end>", "trigger": "always" }4. Activate
POST /workflows/<wf>/activateTừ bây giờ, việc gửi một version của item articles sẽ tự động khởi động luồng. Phê duyệt qua tất cả các cấp sẽ promote version vào bài viết (và chạy action update_field trước); từ chối ở bất kỳ cấp nào sẽ kết thúc instance với trạng thái rejected và không chạm vào bài viết.
Capabilities demonstrated
| Tính năng | Vị trí |
|---|---|
| Tự động khởi động khi gửi version | trigger_event: versions.submit (+ trigger_filter có thể thấy delta) |
| Phân nhánh có điều kiện | hai transition condition từ start, cuối cùng là catch-all |
| Phê duyệt nhiều cấp tuần tự | legal → editor → chief |
| Ba chế độ approval | all, any, count(2) |
| Gán theo role | assign_type: role trên mỗi cấp |
| Tất cả timeout actions | escalate (legal, chief) + nhắc nhở notify (editor) |
| Hành động tự động | update_field trên mark_reviewed |
| Tự động promote / từ chối | node end duy nhất, phân nhánh theo trigger của transition |
| Ủy quyền | có thể dùng khi runtime qua POST .../delegate (không cần cấu hình) |