Appearance
Workflow Engine
Overview
The Workflow Engine (src/modules/workflow/) implements a visual approval workflow system. It supports:
- Workflow Definitions with steps and transitions
- Instances — running executions tied to a specific collection item
- Task Inbox — assigned pending tasks for users
- Audit Log — complete history of all actions
Core Concepts
Trigger Events
| Event | Description |
|---|---|
items.create | Auto-trigger when an item is created in the bound collection |
items.update | Auto-trigger on update (with optional filter) |
manual | Triggered explicitly via POST /workflow-instances |
Step Types
| Type | Description |
|---|---|
start | Entry point of the workflow |
approval | Requires human approval with configurable assignees |
condition | Routes flow based on a condition expression |
action | Executes an automated action (e.g., update item, send notification) |
notification | Sends a notification without waiting for approval |
end | Terminal step (completes the instance) |
Assignment Types (for Approval steps)
| Type | Description |
|---|---|
role | Assigns to all users with a specific role |
user | Assigns to a specific user UUID |
field | Reads the assignee from a field on the item |
creator | Assigns to the user who started the instance |
auto | Auto-approves (used with auto_approve timeout action) |
Approval Modes
| Mode | Description |
|---|---|
any | Any one assignee approves |
all | All assignees must approve |
majority | More than half must approve |
count | A specific number (set in approval_count) |
Data Model
odp_workflows
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
name | varchar | Display name |
description | text | Description |
collection | varchar | Bound collection (nullable for manual workflows) |
status | varchar | draft, active, archived |
trigger_event | varchar | items.create, items.update, manual |
trigger_filter | json | Filter conditions for auto-trigger |
options | json | Additional configuration |
created_by | UUID | Creator user UUID |
created_at | timestamp | |
updated_at | timestamp |
odp_workflow_steps
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
workflow_id | UUID | FK to odp_workflows.id |
key | varchar | Unique step key within workflow |
name | varchar | Display name |
type | varchar | Step type |
assign_type | varchar | Assignment method |
assign_value | varchar | Role UUID, user UUID, or field name |
approval_mode | varchar | Approval mode |
approval_count | integer | Required approvals for count mode |
timeout_minutes | integer | Timeout in minutes (null = no timeout) |
timeout_action | varchar | escalate, auto_approve, auto_reject, notify |
escalate_to | varchar | User UUID for escalation |
action_type | varchar | For action steps |
action_config | json | Action configuration |
position_x | integer | Visual canvas position |
position_y | integer | Visual canvas position |
sort_order | integer | Step order |
odp_workflow_transitions
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
workflow_id | UUID | FK to workflow |
from_step_id | UUID | Source step |
to_step_id | UUID | Destination step |
trigger | varchar | approve, reject, timeout, condition, always |
condition | json | Condition expression (for condition trigger) |
sort_order | integer | Priority when multiple transitions match |
label | varchar | Display label on the canvas edge |
odp_workflow_instances
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
workflow_id | UUID | FK to workflow definition |
collection | varchar | Collection the item belongs to |
item_id | varchar | Primary key of the bound item |
status | varchar | running, completed, rejected, cancelled, error |
current_step_id | UUID | Active step (nullable when completed) |
started_by | UUID | User who triggered the instance |
started_at | timestamp | |
completed_at | timestamp |
odp_workflow_instance_steps
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
instance_id | UUID | FK to instance |
step_id | UUID | FK to workflow step |
status | varchar | pending, active, approved, rejected, skipped, timed_out |
approvals | json | Array of {user_id, action, comment, timestamp} |
activated_at | timestamp | When this step became active |
completed_at | timestamp | When this step was resolved |
timeout_at | timestamp | Timeout deadline |
odp_workflow_actions
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
instance_id | UUID | FK to instance |
step_id | UUID | FK to step (nullable for instance-level actions) |
user_id | UUID | Acting user |
action | varchar | start, approve, reject, delegate, comment, escalate, cancel, auto_approve, timeout |
comment | text | Optional comment |
data | json | Additional action data |
created_at | timestamp |
Workflow Definition Endpoints
POST /workflows
Create a workflow definition. Admin only + workflow.manage permission.
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
List workflow definitions.
Auth required: workflow.view permission
Query Parameters
| Param | Description |
|---|---|
status | Filter by status |
collection | Filter by collection |
limit | Max results |
offset | Pagination |
GET /workflows/:id
Read a workflow definition (includes steps and 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
Update a workflow definition. Admin only.
DELETE /workflows/:id
Delete a workflow. Admin only. Cannot delete workflows with running instances.
POST /workflows/:id/duplicate
Duplicate a workflow (creates a new draft). Admin only.
Response 201 — { "data": { "id": "new-workflow-uuid" } }
POST /workflows/:id/activate
Activate a workflow (status draft → active). Admin only.
Response 200 — { "data": { "id": "...", "status": "active" } }
POST /workflows/:id/deactivate
Deactivate a workflow (status active → draft). Admin only.
Step Endpoints
POST /workflows/:id/steps
Add a step to a workflow. Admin only.
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
Update a step.
DELETE /workflows/:id/steps/:key
Remove a step.
Transition Endpoints
POST /workflows/:id/transitions
Add a transition between steps. Admin only.
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
Update a transition.
DELETE /workflows/:id/transitions/:tid
Remove a transition.
Instance Endpoints
POST /workflow-instances
Start a workflow instance for an item.
Auth required: workflow.start permission on the 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
List instances.
Auth required: workflow.view permission
Query Parameters
| Param | Description |
|---|---|
workflow_id | Filter by workflow |
collection | Filter by collection |
item_id | Filter by item |
status | Filter by status |
limit / offset | Pagination |
GET /workflow-instances/:id
Read a single instance (includes step states, actions, workflow steps).
Auth required: workflow.view OR workflow.participate
POST /workflow-instances/:id/cancel
Cancel a running instance. Admin only.
Request Body
json
{
"comment": "Cancelling due to outdated content"
}Step Action Endpoints
These endpoints allow workflow participants to act on pending steps.
POST /workflow-instances/:id/steps/:stepId/approve
Approve the current step.
Auth required: workflow.participate on the collection
Request Body
json
{
"comment": "Looks good, approved."
}Response 200 — { "data": { "success": true } }
After approval, the engine evaluates transitions to advance to the next step automatically.
POST /workflow-instances/:id/steps/:stepId/reject
Reject the current step.
Auth required: workflow.participate
Request Body
json
{
"comment": "Content needs revision."
}Rejection comment is required.
POST /workflow-instances/:id/steps/:stepId/delegate
Delegate the task to another user.
Auth required: workflow.participate
Request Body
json
{
"to_user": "colleague-user-uuid",
"comment": "Delegating to editor"
}The target user must also have workflow.participate permission.
POST /workflow-instances/:id/steps/:stepId/comment
Add a comment without taking action.
Auth required: workflow.participate
Request Body
json
{
"comment": "Please review section 3 carefully."
}Task Inbox Endpoints
GET /workflow-tasks
Get the current user's pending tasks (their personal inbox).
Auth required: workflow.participate
Query Parameters
| Param | Description |
|---|---|
limit | Max results |
offset | Pagination |
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 }
}Important: Use step_id (not id) when calling approve/reject/delegate endpoints.
GET /workflow-tasks/count
Get the count of the current user's pending tasks (for badge display).
Response 200
json
{
"data": { "count": 3 }
}GET /workflow-tasks/all
Get all active tasks across all users. Admin only.
Audit Log Endpoints
GET /workflow-instances/:id/actions
Get the full action history for a workflow instance.
Auth required: workflow.view OR 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
Get all actions across all instances. Admin only + workflow.manage.
Query Parameters
| Param | Description |
|---|---|
instance_id | Filter by instance |
action | Filter by action type |
limit / offset | Pagination |
App Permissions
Workflow endpoints check app-level permissions stored in odp_app_permissions:
| Action | Who | Description |
|---|---|---|
workflow.view | View-only users | Read workflows and instances |
workflow.start | Contributors | Start new instances |
workflow.participate | Reviewers | Approve/reject/delegate tasks |
workflow.manage | Managers | Create/edit/delete workflow definitions |
Disable app permission checks by setting ENABLE_APP_PERMISSIONS=false.
Scheduled Features
The workflow scheduler (registerWorkflowScheduler()) runs periodically to:
Detect timed-out steps — Steps with
timeout_at < NOWare handled according totimeout_action:auto_approve— Automatically approves the stepauto_reject— Automatically rejectsescalate— Delegates toescalate_tousernotify— Sends a notification but keeps step active
Advance instances — After a timeout action, the engine re-evaluates transitions.
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!" }