Skip to content

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

EventDescription
items.createAuto-trigger when an item is created in the bound collection
items.updateAuto-trigger on update (with optional filter)
manualTriggered explicitly via POST /workflow-instances

Step Types

TypeDescription
startEntry point of the workflow
approvalRequires human approval with configurable assignees
conditionRoutes flow based on a condition expression
actionExecutes an automated action (e.g., update item, send notification)
notificationSends a notification without waiting for approval
endTerminal step (completes the instance)

Assignment Types (for Approval steps)

TypeDescription
roleAssigns to all users with a specific role
userAssigns to a specific user UUID
fieldReads the assignee from a field on the item
creatorAssigns to the user who started the instance
autoAuto-approves (used with auto_approve timeout action)

Approval Modes

ModeDescription
anyAny one assignee approves
allAll assignees must approve
majorityMore than half must approve
countA specific number (set in approval_count)

Data Model

odp_workflows

ColumnTypeDescription
idUUIDPrimary key
namevarcharDisplay name
descriptiontextDescription
collectionvarcharBound collection (nullable for manual workflows)
statusvarchardraft, active, archived
trigger_eventvarcharitems.create, items.update, manual
trigger_filterjsonFilter conditions for auto-trigger
optionsjsonAdditional configuration
created_byUUIDCreator user UUID
created_attimestamp
updated_attimestamp

odp_workflow_steps

ColumnTypeDescription
idUUIDPrimary key
workflow_idUUIDFK to odp_workflows.id
keyvarcharUnique step key within workflow
namevarcharDisplay name
typevarcharStep type
assign_typevarcharAssignment method
assign_valuevarcharRole UUID, user UUID, or field name
approval_modevarcharApproval mode
approval_countintegerRequired approvals for count mode
timeout_minutesintegerTimeout in minutes (null = no timeout)
timeout_actionvarcharescalate, auto_approve, auto_reject, notify
escalate_tovarcharUser UUID for escalation
action_typevarcharFor action steps
action_configjsonAction configuration
position_xintegerVisual canvas position
position_yintegerVisual canvas position
sort_orderintegerStep order

odp_workflow_transitions

ColumnTypeDescription
idUUIDPrimary key
workflow_idUUIDFK to workflow
from_step_idUUIDSource step
to_step_idUUIDDestination step
triggervarcharapprove, reject, timeout, condition, always
conditionjsonCondition expression (for condition trigger)
sort_orderintegerPriority when multiple transitions match
labelvarcharDisplay label on the canvas edge

odp_workflow_instances

ColumnTypeDescription
idUUIDPrimary key
workflow_idUUIDFK to workflow definition
collectionvarcharCollection the item belongs to
item_idvarcharPrimary key of the bound item
statusvarcharrunning, completed, rejected, cancelled, error
current_step_idUUIDActive step (nullable when completed)
started_byUUIDUser who triggered the instance
started_attimestamp
completed_attimestamp

odp_workflow_instance_steps

ColumnTypeDescription
idUUIDPrimary key
instance_idUUIDFK to instance
step_idUUIDFK to workflow step
statusvarcharpending, active, approved, rejected, skipped, timed_out
approvalsjsonArray of {user_id, action, comment, timestamp}
activated_attimestampWhen this step became active
completed_attimestampWhen this step was resolved
timeout_attimestampTimeout deadline

odp_workflow_actions

ColumnTypeDescription
idUUIDPrimary key
instance_idUUIDFK to instance
step_idUUIDFK to step (nullable for instance-level actions)
user_idUUIDActing user
actionvarcharstart, approve, reject, delegate, comment, escalate, cancel, auto_approve, timeout
commenttextOptional comment
datajsonAdditional action data
created_attimestamp

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

ParamDescription
statusFilter by status
collectionFilter by collection
limitMax results
offsetPagination

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 draftactive). Admin only.

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


POST /workflows/:id/deactivate

Deactivate a workflow (status activedraft). 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

ParamDescription
workflow_idFilter by workflow
collectionFilter by collection
item_idFilter by item
statusFilter by status
limit / offsetPagination

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

ParamDescription
limitMax results
offsetPagination

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

ParamDescription
instance_idFilter by instance
actionFilter by action type
limit / offsetPagination

App Permissions

Workflow endpoints check app-level permissions stored in odp_app_permissions:

ActionWhoDescription
workflow.viewView-only usersRead workflows and instances
workflow.startContributorsStart new instances
workflow.participateReviewersApprove/reject/delegate tasks
workflow.manageManagersCreate/edit/delete workflow definitions

Disable app permission checks by setting ENABLE_APP_PERMISSIONS=false.


Scheduled Features

The workflow scheduler (registerWorkflowScheduler()) runs periodically to:

  1. Detect timed-out steps — Steps with timeout_at < NOW are handled according to timeout_action:

    • auto_approve — Automatically approves the step
    • auto_reject — Automatically rejects
    • escalate — Delegates to escalate_to user
    • notify — Sends a notification but keeps step active
  2. 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!" }

ODP Internal API Documentation