Skip to content

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

EventMô tả
items.createTự động kích hoạt khi một item được tạo trong collection đã liên kết
items.updateTự động kích hoạt khi item được cập nhật (có thể kèm filter)
versions.submitTự động kích hoạt khi một version được gửi để xét duyệt (xem Content Moderation)
manualKích hoạt thủ công qua POST /workflow-instances

Step Types

LoạiMô tả
startĐiểm bắt đầu của workflow
approvalYê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
actionThực thi một hành động tự động (ví dụ: cập nhật item, gửi thông báo)
notificationGửi thông báo mà không chờ phê duyệt
endStep 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 (usually start), not with a condition node. See Branching.
  • Notifications are sent with an action step (action_type: "send_notification"), not a notification node.

Assignment Types (for Approval steps)

LoạiMô tả
roleGán cho tất cả người dùng có một role cụ thể
userGá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)
creatorGán cho người dùng đã khởi động instance
autoTự động phê duyệt (dùng kèm với timeout action auto_approve)

Approval Modes

Chế độMô tả
anyBất kỳ một người được gán nào phê duyệt là đủ
allTất cả người được gán đều phải phê duyệt
majorityHơn một nửa phải phê duyệt
countMộ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' 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ĐếnKết quả
approvalapproveendinstance completed, version được promote vào item chính
approvalrejectend (cùng node)instance rejected, version được đánh dấu rejected, không promote

Reject đến end là an toàn — không bao giờ promote, vì promotion bị chặn bởi điều kiện trigger === 'approve'. Template thông báo cũng theo trigger: workflow_completed khi approve, workflow_task_rejected khi 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 approveend 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:

  1. Lượt 1 — khớp transition có trigger bằng trigger advance (approve / reject) hoặc always, theo thứ tự sort_order.
  2. 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_typeaction_configGhi 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_notificationpayload cho notify.trigger (template_id, recipients, ...)Phát event notify, không chờ
promote_versionPromote 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 / handleTimeout await toàn bộ chuỗi — advance step hiện tại → activate step tiếp theo → chạy actions → đến end → 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ằng setImmediate, 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.promote chạ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ấu completed và 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 error version được reset về draft (failInstanceInTrx). Không reset thì version kẹt ở pending (vì submit() chỉ nhận draft/rejected và instance error là terminal) → editor không resubmit được. Sau khi release, submit lại sẽ tạo instance mới (instance error cũ giữ làm lịch sử; chỉ instance rejected mớ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)
FromTriggerToResult
approvalapproveendinstance completed, version promoted into the main item
approvalrejectend (same node)instance rejected, version marked rejected, not promoted

Reject reaching end is safe — it never promotes, because promotion is gated on trigger === 'approve'. The notification template also follows the trigger: workflow_completed on approve, workflow_task_rejected on 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 approveend transition handle it is enough.

Data Model

odp_workflows

CộtKiểuMô tả
idUUIDKhóa chính
namevarcharTên hiển thị
descriptiontextMô tả
collectionvarcharCollection liên kết (nullable với workflow thủ công)
statusvarchardraft, active, archived
trigger_eventvarcharitems.create, items.update, versions.submit, manual
trigger_filterjsonĐiều kiện filter cho auto-trigger
optionsjsonCấu hình bổ sung
created_byUUIDUUID người tạo
created_attimestamp
updated_attimestamp

odp_workflow_steps

CộtKiểuMô tả
idUUIDKhóa chính
workflow_idUUIDFK đến odp_workflows.id
keyvarcharKhóa step duy nhất trong workflow
namevarcharTên hiển thị
typevarcharLoại step
assign_typevarcharPhương thức gán
assign_valuevarcharUUID role, UUID người dùng, hoặc tên field
approval_modevarcharChế độ phê duyệt
approval_countintegerSố lượt phê duyệt yêu cầu cho chế độ count
timeout_minutesintegerThời gian timeout tính bằng phút (null = không có timeout)
timeout_actionvarcharescalate, auto_approve, auto_reject, notify
escalate_tovarcharUUID người dùng để leo thang
action_typevarcharDùng cho bước action
action_configjsonCấu hình hành động
position_xintegerVị trí trên canvas trực quan
position_yintegerVị trí trên canvas trực quan
sort_orderintegerThứ tự step

odp_workflow_transitions

CộtKiểuMô tả
idUUIDKhóa chính
workflow_idUUIDFK đến workflow
from_step_idUUIDStep nguồn
to_step_idUUIDStep đích
triggervarcharapprove, reject, timeout, condition, always
conditionjsonBiểu thức điều kiện (cho trigger condition)
sort_orderintegerĐộ ưu tiên khi nhiều transition khớp
labelvarcharNhãn hiển thị trên cạnh của canvas

odp_workflow_instances

CộtKiểuMô tả
idUUIDKhóa chính
workflow_idUUIDFK đến workflow definition
collectionvarcharCollection mà item thuộc về
item_idvarcharKhóa chính của item liên kết
version_idvarcharVersion đang được xét duyệt (nullable — null với workflow không có version, xem Content Moderation)
statusvarcharrunning, completed, rejected, cancelled, error
current_step_idUUIDStep đang active (nullable khi đã hoàn tất)
started_byUUIDNgười dùng đã kích hoạt instance
started_attimestamp
completed_attimestamp

odp_workflow_instance_steps

CộtKiểuMô tả
idUUIDKhóa chính
instance_idUUIDFK đến instance
step_idUUIDFK đến workflow step
statusvarcharpending, active, approved, rejected, skipped, timed_out
approvalsjsonMảng {user_id, action, comment, timestamp}
activated_attimestampThời điểm step này trở thành active
completed_attimestampThời điểm step này được giải quyết
timeout_attimestampThời hạn timeout

odp_workflow_actions

CộtKiểuMô tả
idUUIDKhóa chính
instance_idUUIDFK đến instance
step_idUUIDFK đến step (nullable cho các hành động ở cấp instance)
user_idUUIDNgười dùng thực hiện hành động
actionvarcharstart, approve, reject, delegate, comment, escalate, cancel, auto_approve, timeout
commenttextBình luận tùy chọn
datajsonDữ liệu hành động bổ sung
created_attimestamp

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ả
statusLọc theo status
collectionLọc theo collection
limitSố kết quả tối đa
offsetPhâ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 draftactive). Chỉ Admin.

Response 200{ "data": { "id": "...", "status": "active" } }


POST /workflows/:id/deactivate

Vô hiệu hóa một workflow (status activedraft). 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_idLọc theo workflow
collectionLọc theo collection
item_idLọc theo item
statusLọc theo status
limit / offsetPhâ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ả
limitSố kết quả tối đa
offsetPhâ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_idLọc theo instance
actionLọc theo loại hành động
limit / offsetPhâ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ượngMô tả
workflow.viewNgười dùng chỉ xemĐọc workflows và instances
workflow.startNgười đóng gópKhởi động instance mới
workflow.participateNgười xét duyệtApprove/reject/delegate tasks
workflow.manageQuả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ỳ để:

  1. Phát hiện step đã timeout — Các step có timeout_at < NOW được xử lý theo timeout_action:
    • auto_approve — Tự động phê duyệt step
    • auto_reject — Tự động từ chối
    • escalate — Gán lại cho escalate_to một lần, rồi xóa timeout_at (null) để không bị timeout lại. Escalate là one-shot (model chỉ có một cấp escalate_to); muốn nhắc lặp lại thì dùng notify.
    • notify — Gửi workflow_reminder cho người được gán và re-arm timeout_at cho 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'.

  1. 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>/activate

Từ 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ăngVị trí
Tự động khởi động khi gửi versiontrigger_event: versions.submit (+ trigger_filter có thể thấy delta)
Phân nhánh có điều kiệnhai 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ế độ approvalall, any, count(2)
Gán theo roleassign_type: role trên mỗi cấp
Tất cả timeout actionsescalate (legal, chief) + nhắc nhở notify (editor)
Hành động tự độngupdate_field trên mark_reviewed
Tự động promote / từ chốinode end duy nhất, phân nhánh theo trigger của transition
Ủy quyềncó thể dùng khi runtime qua POST .../delegate (không cần cấu hình)

ODP Internal API Documentation