Eviworx
Docs

Workflows API

The Workflows API enables automation of business processes with visual workflows, multi-step approvals, timer events, parallel branches, retry of failed steps, pausing and 7 integrated actions (email, webhooks, tickets, etc.). Templates support versioning with rollback and publishing.

🚀
Features
✓ 8 node types (approval, timer, gateway …)
✓ 7 actions (email, webhook, ticket …)
✓ Visual drag-and-drop designer
✓ Auto-approval (field value, permission, role)
✓ Conditional routing (18 operators)
✓ Retry of failed steps
✓ Pausing (no further steps)
✓ Versioning with rollback
✓ Activity log with audit trail (bilingual)
✓ Start by external systems (API key)

Endpoints Overview

Workflow Instances

Method Endpoint Description
GET/api/workflow/instancesList all instances (with filtering)
GET/api/workflow/instances/:idGet single instance (with steps & activities)
GET/api/workflow/instances/statsCounters within your visibility scope + your open tasks
POST/api/workflow/instances/:id/steps/:stepId/completeComplete step (approval steps go through /approve)
POST/api/workflow/instances/:id/steps/:stepId/approveRecord approval decision
POST/api/workflow/instances/:id/cancelCancel workflow (202, no body)
POST/api/workflow/instances/:id/pausePause workflow; no steps run until it is resumed (202, no body)
POST/api/workflow/instances/:id/resumeResume workflow (202, no body)
POST/api/workflow/instances/:id/steps/:stepId/retryRetry failed step
POST/api/workflow/instances/:id/steps/:stepId/reassignReassign step to another user

Workflows are started via one of the two start paths — users through POST /api/workflow/catalog/:templateId/start, external systems by API key through POST /api/workflow/api/trigger/:templateId. Both answer 202 Accepted; the workflow engine executes asynchronously. Same for cancel/pause/resume: 202 with no body.

Templates (CRUD & Publishing)

Method Endpoint Description
GET/api/workflow/templatesList all templates (filters + pagination; ?includeDeleted=true shows the trash)
GET/api/workflow/templates/statisticsCounters across all templates: total, published, draft, archived, inactive (?includeDeleted)
GET/api/workflow/templates/categoriesDistinct categories across all templates ({data: string[]}) — source for the category filters
GET/api/workflow/templates/:idGet single template with steps
POST/api/workflow/templatesCreate template (drag-&-drop editor)
PUT/api/workflow/templates/:idUpdate template
DELETE/api/workflow/templates/:idDelete template (soft-delete)
POST/api/workflow/templates/:id/restoreRestore template (requires restoreTemplates AND viewDeletedTemplates)
POST/api/workflow/templates/:id/publishPublish template
POST/api/workflow/templates/:id/unpublishUnpublish template
PUT/api/workflow/templates/:id/permissionsSet template permissions

Versioning

Method Endpoint Description
POST/api/workflow/templates/:id/versionsCreate new version (semver, changelog)
POST/api/workflow/templates/:name/rollbackRollback to a previous version
POST/api/workflow/templates/:id/publish · /unpublishPublish / unpublish

My Tasks

A user's open workflow steps are returned by GET /api/my-tasks?types=WORKFLOW_STEP. Instance statistics are served by GET /api/workflow/instances/stats.

Activity & Audit

Method Endpoint Description
GET/api/workflow/activity/historyGlobal workflow history
GET/api/workflow/activity/statisticsWorkflow statistics
Activities of a single instance come embedded from GET /api/workflow/instances/:id.

Workflow Catalog

Method Endpoint Description
GET/api/workflow/catalog/listStartable workflows: flat list ({data, pagination}, search & category filter)
POST/api/workflow/catalog/:templateId/startStart workflow from catalog (202)

API Trigger (External Systems)

Method Endpoint Description
POST/api/workflow/api/trigger/:templateIdTrigger workflow via API key

Workflow Concepts

Templates vs. Instances

WorkflowTemplate:
├─ Blueprint/Definition (reusable)
├─ Contains step definitions (array of nodes)
├─ Versioned (v1, v2, v3...)
└─ Can be published/unpublished

WorkflowInstance:
├─ Concrete execution of a template
├─ Has own runtime data (data, variables)
├─ Creates StepExecutions (one per step)
├─ Status: DRAFT → RUNNING → COMPLETED/FAILED/CANCELLED
└─ Has activities (audit trail)

Workflow Status

Status Description
DRAFTCreated but not started yet
RUNNINGActively running, steps being executed
PAUSEDTemporarily paused, can be resumed
COMPLETEDSuccessfully completed
CANCELLEDCancelled by user
FAILEDFailed (e.g., approval rejected without escalation)

Step Status

Status Description
PENDINGNot yet reached
ASSIGNEDAssigned, waiting for user action
IN_PROGRESSCurrently being processed
COMPLETEDCompleted
REJECTEDRejected (approval only)
FAILEDFailed (e.g., webhook error)
SKIPPEDSkipped (e.g., unchosen conditional branch)

8 Node Types (Step Types)

1. MANUAL_TASK

Manual task assigned to a user or group. User must manually complete the task.

Properties:
• Assignment: User, group or role-based
• Deadlines: Optional (e.g., "in 24 hours")
• Notifications: Automatic via unified system
• Instructions: Instructions for the user

Flow:
1. Step is set to ASSIGNED
2. User receives notification
3. User completes task (POST .../complete)
4. Step changes to COMPLETED
5. Next step is triggered

2. APPROVAL

Approval step with auto-approve/reject logic, escalation support and manual decision.

Features:Auto-Approve: Field-based (e.g., "if amount < 5000")
• Auto-Reject: Has higher priority than auto-approve
• Escalation: On any rejection to the configured step (escalationStepId)
• Manual Approval: If no auto-rule applies
• failOnReject: Workflow FAILED on rejection — escalation takes precedence

Flow (Auto-Reject):
1. Check auto-reject condition (FIRST priority)
2. If TRUE: Reject + escalation (if configured)
3. If escalation missing: Workflow FAILED

Flow (Auto-Approve):
1. Check auto-reject condition (FALSE)
2. Check auto-approve condition (SECOND priority)
3. If TRUE: Immediately APPROVED + next step

Flow (Manual):
1. No auto-rule applies
2. Step is set to ASSIGNED
3. Approver receives notification
4. Approver decides (POST .../approve)
5. On APPROVED: Next step
6. On REJECTED: Escalation or Workflow FAILED

3. AUTOMATED_ACTION

Automated action - executes one of 7 integrated actions.

7 Action Types:

1. send_email      = Send email (via notification worker)
2. webhook         = HTTP request (POST/GET/PUT), SSRF-protected
3. create_ticket   = Create new ticket
4. update_ticket   = Update ticket (status, priority, assignee)
5. add_ticket_comment = Add comment to ticket
6. update_field    = Update workflow field (data)
7. assign_ticket   = Auto-assign ticket (via assignment engine)

Security:SSRF-Protection: Webhooks block private IPs (127.0.0.1, 10.x, 192.168.x)
• Timeout: Default 10s, configurable up to 60s
• Redirects: Disabled (redirect: "error")
• Variable Replacement: {{"{{"}}field{{"}}"}} replaced by workflow.data.field

Flow:
1. Step changes to IN_PROGRESS
2. Action is executed (with timeout protection)
3. On success: COMPLETED + outputData saved
4. On error: FAILED + errorMessage
5. Next step is triggered (only on success)

4. NOTIFICATION

Sends notifications to users or groups (via unified notification system).

Recipient Types:userIds: Array of user IDs
• groupId: All members of a group
• roleName: All users with this role
• notifyInitiator: Notify workflow initiator
• recipientField: Read dynamically from workflow.data

Notification Types:task: General task notification (default)
• reminder: Reminder notification (e.g., "deadline in 2h")
• escalation: Escalation notification

Flow:
1. Recipients are resolved (deduplicated)
2. For each recipient: Notification via unified system
3. Step completes immediately (no waiting)
4. outputData: { notificationsSent: X, totalRecipients: Y }

5. PARALLEL_GATEWAY

Starts multiple workflow branches in parallel. All branches execute simultaneously.

Properties:nextSteps: Array of step IDs (parallel branches)
• No synchronization at end (no "join")
• Workflow is COMPLETED when ALL branches are completed

Flow:
1. Gateway step changes to COMPLETED
2. WorkflowEngine starts ALL nextSteps simultaneously
3. Each branch runs independently
4. Workflow status stays RUNNING until all branches done

Example:
Gateway → [Branch A: Legal Approval, Branch B: Finance Approval]
Both approvals run in parallel, independently of each other

6. CONDITIONAL_BRANCH

Conditional branching - selects one of multiple paths based on conditions.

18 supported operators:

Equality: equals, notEquals
Numeric: greaterThan, lessThan, greaterOrEqual, lessOrEqual, between
String: contains, notContains, startsWith, endsWith, matches (regex)
List: in, notIn
Empty/null: isNull, isNotNull, isEmpty, isNotEmpty

in and notIn expect a non-empty value list, between exactly two bounds;the four empty/null operators do not read value. With valueType (string,number, boolean) both sides are converted before comparing — without itequals/notEquals compare strictly ("5" is not 5).
Complex Conditions (Nested):
{
  "type": "AND",
  "conditions": [
    { "type": "field", "field": "trigger.priority", "operator": "equals", "value": "HIGH" },
    { "type": "field", "field": "trigger.amount", "operator": "greaterThan", "value": 10000 }
  ]
}

Flow:
1. Conditions are evaluated in order
2. First match = Selected branch
3. No match = Default branch (if configured)
4. No match + no default = FAILED
5. Selected branch is triggered (overrideNextSteps)

Security:
• Max Recursion Depth: 10 (prevents stack overflow)
• Max Field Depth: 10 (prevents DoS via "a.b.c.d.e...")
• Dangerous keys blocked: __proto__, constructor, prototype
• Regex limit: 500 characters

7. DATA_COLLECTION

Form step - presents a form and collects user input.

Properties:formSchema: JSON schema for form fields
• prefillFields: Prefill fields from workflow.data
• Validierung: server-side on completion, against the formSchema

Flow:
1. Step is set to ASSIGNED
2. User receives notification
3. User fills out form
4. POST .../complete with data
5. Backend validates data against formSchema
6. On success: the validated values land in data.stepOutputs[stepName]
7. Step COMPLETED + next step

Example formSchema:
{
  "requestReason": {
    "type": "textarea",
    "label": "Reason",
    "required": true,
    "minLength": 50
  },
  "urgency": {
    "type": "select",
    "label": "Urgency",
    "options": ["LOW", "MEDIUM", "HIGH"]
  }
}

8. TIMER_EVENT

Timer step - delays the workflow by a fixed time or waits until a specific time.

3 Timer Types (timerType):

1. duration = Relative delay from the start of the step (e.g., "3 hours")
   • days: 0
   • hours: 3
   • minutes: 30
   • seconds: 0

2. datetime = Absolute point in time, ISO-8601 with time zone (Z or ±HH:MM)
   • datetime: "2026-02-01T09:00:00Z"

3. expression = Point in time taken from the workflow data; the resolved value must carry a time zone
   • expression: "{{trigger.dueDate}}"

Flow:
1. Step changes to IN_PROGRESS
2. TimerJob is created (persistent in DB)
3. Step waits (shouldComplete = false)
4. TimerChecker service checks every 30s for due timers
5. When executeAt reached: Step is set to COMPLETED
6. Next step is triggered

Special Case:
• Delay = 0 or timestamp already past → Immediate completion
• No CRON support (use the CronJob system for that)

API Examples

Start Workflow

POST /api/workflow/catalog/:templateId/start
{
  "data": {
    "requestType": "Hardware",
    "amount": 12500,
    "justification": "New laptops for development team"
  }
}

data is validated against the template's triggerSchema (required fields → 400 FORM_VALIDATION_FAILED) and lands on the instance under data.trigger. The template start permission, the per-user concurrent instance limit and the workflow data limits are enforced as well.

Response (202 Accepted)

{
  "message": "Workflow started successfully",
  "templateId": "clx...",
  "templateName": "Hardware Purchase Request"
}

The start runs asynchronously through the workflow engine — the response therefore carries no instance ID yet. The resulting instance shows up in GET /api/workflow/instances.

Complete Manual Task

POST /api/workflow/instances/:instanceId/steps/:stepId/complete
{
  "outcome": "COMPLETED",
  "data": {
    "budgetLineItem": "IT-Equipment-2026",
    "approvalCode": "FIN-2026-045"
  }
}

outcome ∈ APPROVED | REJECTED | COMPLETED | FAILED (optional). data carries the step input — for DATA_COLLECTION the form fields, validated against the formSchema. Response: the completed step execution ({id, status, completedAt}).

An APPROVAL step is decided through /approve, not through this endpoint (400 APPROVAL_STEP_REQUIRES_DECISION). That way every approval decision takes the same path — same permission check, same comment requirement, same rejection handling.

Approval Decision

POST /api/workflow/instances/:instanceId/steps/:stepId/approve
{
  "decision": "APPROVED",
  "comment": "Approved based on business justification and available budget"
}

// OR

{
  "decision": "REJECTED",
  "comment": "Budget exceeded for Q1, resubmit in Q2"
}

decision is exactly APPROVED or REJECTED (400 INVALID_APPROVAL_DECISION). Beyond the assignment, the decider needs the workflows.completeSteps permission. If the step sets requireRejectionComment, a rejection requires a comment (400 REJECTION_COMMENT_REQUIRED). Response: {decision, complete} — complete=false means further approvals are still pending. The comment — on approval as well as on rejection — lands in the instance under data.stepOutputs[step name].comments and is visible there in the overview and the activity trail.

Trigger Workflow via API Key (External Systems)

POST /api/workflow/api/trigger/:templateId
X-API-Key: your-api-key-here
{
  "data": {
    "externalSystemId": "SAP-12345",
    "purchaseOrderNumber": "PO-2026-1234",
    "amount": 25000,
    "vendor": "Dell Technologies"
  },
  "metadata": {
    "source": "SAP",
    "correlationId": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2026-01-27T15:30:00Z"
  }
}
  • Auth: X-API-Key header. The API key's role needs the workflows.startWorkflow permission AND — if the template carries start permissions — must be listed in its allowedRoles (allowedUsers only applies to user starts, not to API keys; if both lists are empty the global permission is enough).
  • Body: data (optional, validated against the template's triggerSchema, size-capped; lands on the instance under data.trigger) · metadata (optional: source, correlationId (UUID), timestamp)
  • Response: 202 Accepted — the instance is created and processed asynchronously by the workflow engine.
  • The template must carry triggerType API — with any other value the route answers 400 INVALID_TRIGGER_TYPE. The field has exactly two values: MANUAL (started from the catalog) and API (started by an external system); any other value is rejected with 400 when saving the template.

Get Workflow Catalog

GET /api/workflow/catalog/list?category=Procurement&search=hardware&page=1&limit=20

Response

{
  "data": [
    {
      "id": "clx...",
      "name": "Hardware Purchase Request",
      "description": "Multi-step approval for hardware purchases > €5,000",
      "category": "Procurement",
      "version": 3,
      "isActive": true,
      "isPublished": true,
      "triggerType": "MANUAL",
      "triggerSchema": { "fields": [ "..." ] }
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 7, "totalPages": 1, "hasMore": false }
}

The catalog is the end-user surface: it requires workflows.startWorkflow and only returns published, active templates the caller may actually start (allowedRoles/allowedUsers). Template MANAGEMENT (GET /api/workflow/templates with the full step definitions) requires workflows.viewTemplates instead.

Get Instance with Details

GET /api/workflow/instances/:id

Response (Full Instance)

{
  "id": "clx...",
  "templateId": "clx...",
  "status": "RUNNING",
  "priority": "HIGH",
  "currentSteps": ["step-3"],
  "completedSteps": {
    "step-1": { "status": "COMPLETED", "outcome": "COMPLETED", "startedAt": "2026-01-27T15:30:00Z", "completedAt": "2026-01-27T15:35:00Z", "errorMessage": null },
    "step-2": { "status": "COMPLETED", "outcome": "APPROVED",  "startedAt": "2026-01-27T15:35:00Z", "completedAt": "2026-01-27T16:20:00Z", "errorMessage": null }
  },
  "data": {
    "trigger": { "requestType": "Hardware", "amount": 12500 },
    "variables": { "budgetLineItem": "IT-Equipment-2026" },
    "stepOutputs": {
      "Submit Request":   { "stepType": "DATA_COLLECTION", "budgetLineItem": "IT-Equipment-2026" },
      "Finance Approval": { "stepType": "APPROVAL", "outcome": "APPROVED", "autoApproved": true }
    }
  },
  "stepDefinitions": [ "... snapshot of the steps at start ..." ],
  "templateVersion": 3,
  "template": {
    "id": "clx...",
    "name": "Hardware Purchase Request",
    "category": "Procurement"
  },
  "stepExecutions": [
    {
      "id": "clx...",
      "stepId": "step-1",
      "stepName": "Submit Request",
      "stepType": "DATA_COLLECTION",
      "status": "COMPLETED",
      "assignedToId": "clx...",
      "completedAt": "2026-01-27T15:35:00Z",
      "outputData": {
        "budgetLineItem": "IT-Equipment-2026"
      }
    },
    {
      "id": "clx...",
      "stepId": "step-2",
      "stepName": "Finance Approval",
      "stepType": "APPROVAL",
      "status": "COMPLETED",
      "outcome": "APPROVED",
      "assignedToId": "clx...",
      "completedAt": "2026-01-27T16:20:00Z",
      "outputData": {
        "autoApproved": true,
        "reason": "Amount below auto-approval threshold"
      }
    },
    {
      "id": "clx...",
      "stepId": "step-3",
      "stepName": "Manager Approval",
      "stepType": "APPROVAL",
      "status": "ASSIGNED",
      "assignedToId": "clx...",
      "assignedAt": "2026-01-27T16:20:00Z"
    }
  ],
  "activities": [
    {
      "id": "clx...",
      "action": "WORKFLOW_STARTED",
      "timestamp": "2026-01-27T15:30:00Z",
      "user": {
        "id": "clx...",
        "name": "John Doe",
        "email": "john@example.com"
      }
    },
    {
      "id": "clx...",
      "action": "STEP_COMPLETED",
      "timestamp": "2026-01-27T15:35:00Z",
      "details": {
        "stepName": "Submit Request",
        "outcome": "COMPLETED"
      }
    },
    {
      "id": "clx...",
      "action": "STEP_AUTO_APPROVED",
      "timestamp": "2026-01-27T16:20:00Z",
      "details": {
        "stepName": "Finance Approval",
        "reason": "Amount below auto-approval threshold"
      }
    }
  ],
  "initiator": {
    "id": "clx...",
    "name": "John Doe",
    "email": "john@example.com"
  },
  "startedAt": "2026-01-27T15:30:00Z",
  "createdAt": "2026-01-27T15:30:00Z",
  "updatedAt": "2026-01-27T16:20:00Z"
}

Running Workflow Data Model

WorkflowInstance.data has exactly three areas: trigger, variables and stepOutputs. stepOutputs is keyed by step name, which is why step names must be unique per template. Each entry carries stepType plus the outputs collected in that step (e.g. comments for MANUAL_TASK and for the decision on an APPROVAL step, form fields for DATA_COLLECTION, a checklist object for checklists).

// WorkflowInstance.data
{
  "trigger":   { /* Trigger/start data from triggerSchema */ },
  "variables": { /* Workflow variables */ },
  "stepOutputs": {
    "Step 1": { "stepType": "MANUAL_TASK", "comments": "Ok cool" },
    "Step 2": { "stepType": "DATA_COLLECTION", "roomNumber": "option4" },
    "Finance Approval": { "stepType": "APPROVAL", "comments": "Budget confirmed" },
    "Step 3": {
      "stepType": "MANUAL_TASK",
      "comments": "Rejected",
      "checklist": { "items": ["..."], "checked": ["..."], "totalCount": 5, "completedCount": 5 }
    }
  }
}

Additionally on the instance: currentSteps[] (active step IDs), completedSteps (map step ID → {status, outcome, startedAt, completedAt, errorMessage}), plus stepDefinitions (snapshot of the step definitions at start) + templateVersion — so template changes do not break running instances. The embedded template relation contains only id, name and category; the step structure of the run is in stepDefinitions.

Templating/paths reference this model: {{ trigger.x }}, {{ variables.y }}, {{ stepOutputs["Step Name"].field }} (dot or bracket notation).

StepExecution

Per step (unique per instance, [workflowInstanceId, stepId]) there is one StepExecution:

  • status: PENDING, ASSIGNED, IN_PROGRESS, COMPLETED, REJECTED, FAILED, SKIPPED · outcome: APPROVED, REJECTED, COMPLETED, FAILED
  • assignmentType: user, group, system, initiator, initiator_manager, previous_step_user, dynamic (+ assignedToId / assignedRole / assignedGroup)
  • inputData, outputData, formData, formSchema (for DATA_COLLECTION), validationErrors, retryCount, comments/rejectionReason
  • On completion, outputData is also mirrored to data.stepOutputs[stepName] (audit trail).

Workflow Engine (Container)

Execution is handled by a dedicated container (workflow-engine). Externally it exposes only /health; it reads and writes all data through the internal backend API: it reads running and overdue instances and steps, creates the follow-up steps, completes steps or marks them as failed, merges variables and processes timers and approvals. Each step type has its own executor (automated, approval, manual, notification, gateway, dataCollection, timer).

  • Timers – checks every 30 seconds which TIMER_EVENT steps have reached their point in time (executeAt)
  • SLA monitoring – overdue steps/instances, escalation
  • Assignment – dynamic assignment (user/role/group, templating against trigger/variables/stepOutputs)
  • Auto-Approval – auto-approve/reject by condition

Advanced Features

Auto-Approval Logic

Approval steps can be automatically approved/rejected based on conditions:

{
  "type": "APPROVAL",
  "name": "Finance Approval",
  "config": {
    "autoApproveConditions": [
      {
        "type": "field",
        "field": "trigger.amount",
        "operator": "lessThan",
        "value": 5000
      }
    ],
    "autoRejectConditions": [
      {
        "type": "field",
        "field": "trigger.priority",
        "operator": "in",
        "value": ["URGENT", "HIGH"]
      }
    ],
    "autoRejectLogic": "OR",
    "failOnReject": true,
    "escalationStepId": "step-escalation"
  }
}

Besides type: "field", auto-decisions also support type: "permission" and type: "role"; checkPermissionsFor selects who is checked — the initiator (default) or the step assignee. Several conditions are combined by autoApproveLogic with AND (default) or OR, and by autoRejectLogic with OR (default) or AND — so a single reason is enough to reject.

Conditions must be evaluable: When a template is saved, the API checks every auto-decision condition and every path condition of a CONDITIONAL_BRANCH. Incomplete conditions are rejected with 400 INVALID_STEP_CONDITIONS; details.conditionErrors names, per row, the step, the surface (autoApprove, autoReject, branchPath), the row number and the reason — missing field, missing or unknown operator, missing permission or role, missing value list for in/notIn, incomplete bounds for between, an empty AND/OR group or nesting deeper than ten levels.

Why the strictness: an incomplete condition always yields the same result at runtime. It therefore counts as not fulfilled — an auto-approval will not apply, a branch path will not be chosen. What cannot take effect should not be stored in the first place.

Webhook with SSRF Protection

{
  "type": "AUTOMATED_ACTION",
  "name": "Notify External System",
  "config": {
    "actionType": "webhook",
    "actionConfig": {
      "url": "https://external-system.com/api/webhook",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer {{"{{"}}apiToken{{"}}"}}",
        "X-Event-Type": "workflow.completed"
      },
      "body": {
        "workflowId": "{{"{{"}}workflowId{{"}}"}}",
        "status": "{{"{{"}}status{{"}}"}}",
        "completedAt": "{{"{{"}}completedAt{{"}}"}}"
      },
      "timeout": 15000
    }
  }
}
Note: Webhook URLs are validated:
  • Private IPs blocked (127.0.0.1, 10.x, 192.168.x)
  • DNS resolution check against SSRF
  • Redirects disabled
  • Timeout protection (default: 10s, max: 60s)

Parallel Approval

{
  "type": "PARALLEL_GATEWAY",
  "name": "Multi-Department Approval",
  "nextSteps": [
    "step-legal-approval",
    "step-finance-approval",
    "step-it-approval"
  ]
}

// All 3 approvals run in parallel
// Workflow is COMPLETED when ALL 3 are approved

Conditional Routing

{
  "type": "CONDITIONAL_BRANCH",
  "name": "Route by Amount",
  "config": {
    "paths": [
      {
        "id": "high-value",
        "name": "CFO",
        "condition": {
          "type": "field",
          "field": "trigger.amount",
          "operator": "greaterThan",
          "value": 50000
        },
        "nextSteps": ["step-cfo-approval"]
      },
      {
        "id": "medium-value",
        "name": "Manager",
        "condition": {
          "type": "field",
          "field": "trigger.amount",
          "operator": "between",
          "value": [10000, 50000]
        },
        "nextSteps": ["step-manager-approval"]
      },
      {
        "id": "rest",
        "name": "Default",
        "isDefault": true,
        "nextSteps": ["step-auto-approve"]
      }
    ]
  }
}

Paths are evaluated in order, first match wins; paths marked isDefault only apply when no condition matched. A default path needs no condition of its own — it is the only path that may omit one. If nothing matches and there is no default path, the step fails.

Timer with Notification

{
  "steps": [
    {
      "id": "step-1",
      "type": "APPROVAL",
      "name": "Approval Required",
      "nextSteps": ["step-2"]
    },
    {
      "id": "step-2",
      "type": "TIMER_EVENT",
      "name": "Wait 24 Hours",
      "config": {
        "timerType": "duration",
        "hours": 24
      },
      "nextSteps": ["step-3"]
    },
    {
      "id": "step-3",
      "type": "NOTIFICATION",
      "name": "Reminder",
      "config": {
        "notifyInitiator": true,
        "message": "Your request has been pending for 24 hours"
      },
      "nextSteps": ["step-4"]
    }
  ]
}

Filtering & Pagination

Filter Instances

GET /api/workflow/instances?status=RUNNING&priority=HIGH&templateId=clx...&search=purchase&page=1&limit=20&sortBy=startedAt&sortOrder=desc
Parameter Description
statusDRAFT, RUNNING, PAUSED, COMPLETED, CANCELLED, FAILED
priorityLOW, MEDIUM, HIGH, URGENT
templateIdFilter by template
initiatorIdFilter by initiator
categoryFilter by template category
searchSearch in template names
pagePage number (default: 1)
limitItems per page (default: 50, max: 100)
sortBystartedAt, status or templateName (default: startedAt)
sortOrderasc or desc (default: desc)

Permissions

Permission Description
workflows.startWorkflowSee the catalog and start workflows (end-user surface; also for API-key triggers)
workflows.viewOwnInstancesView own workflow instances
workflows.viewAllInstancesView all workflow instances
workflows.cancelInstancesCancel/pause/resume instances and retry failed steps
workflows.completeStepsComplete assigned steps and record approval decisions
workflows.reassignStepsReassign steps to another user
workflows.assignableCan be assigned workflow steps
workflows.viewTemplatesTemplate MANAGEMENT: view the template list, statistics and definitions
workflows.createTemplatesCreate templates
workflows.editTemplatesEdit templates, create versions and set start permissions
workflows.publishTemplatesPublish/unpublish templates
workflows.deleteTemplatesDelete templates (soft delete)
workflows.viewDeletedTemplatesSee the trash (?includeDeleted on list and statistics)
workflows.restoreTemplatesRestore deleted templates — together with viewDeletedTemplates

Usage and management have separate permissions: startWorkflow covers the catalog, starting and your own tasks, viewOwn/viewAllInstances the running instances — the complete template definitions (including the webhook configuration of AUTOMATED_ACTION steps) are visible only with viewTemplates.

Two rights additionally apply only to visible instances: cancel, pause, resume and retry only affect instances the caller may see (as initiator or with viewAllInstances). Reassign checks the same on top of the permission.

Error Handling

Common Errors

Error Code HTTP Status Description
WORKFLOW_TEMPLATE_NOT_FOUND404Template ID does not exist
TEMPLATE_INACTIVE403Template is not active
TEMPLATE_NOT_PUBLISHED403Template not published
TEMPLATE_ARCHIVED403Template is archived
CANNOT_EDIT_PUBLISHED_TEMPLATE400Published templates are locked — create a new version instead (exception: isActive and the start permissions)
INVALID_PERMISSIONS_CONFIG400Invalid start permissions; details.validationErrors carries a code + params per row
INVALID_STEP_CONDITIONS400Incomplete condition in a step; details.conditionErrors names step, surface and reason per row
ESCALATION_STEP_NOT_FOUND400escalationStepId points to a step the template does not contain
ESCALATION_CYCLE_DETECTED400The escalation chain forms a loop; details.path shows the cycle
WORKFLOW_INSTANCE_NOT_FOUND404Instance ID does not exist
STEP_NOT_ASSIGNED403Step not assigned to you
STEP_ALREADY_COMPLETED409Step already completed
STEP_INVALID_STATUS400Step is not ASSIGNED/IN_PROGRESS
WORKFLOW_NOT_RUNNING400Workflow is not RUNNING
FORM_VALIDATION_FAILED400Form data invalid
APPROVAL_STEP_REQUIRES_DECISION400Approval step addressed via /complete instead of /approve
INVALID_APPROVAL_DECISION400decision is not APPROVED/REJECTED
REJECTION_COMMENT_REQUIRED400Rejection without a comment although the step requires one
WORKFLOW_DATA_LIMIT_EXCEEDED400Workflow data > 1MB
CONCURRENT_MODIFICATION409Concurrent modification detected (optimistic locking)

Error Example

{
  "error": "Step not assigned to you",
  "errorCode": "STEP_NOT_ASSIGNED",
  "message": "You cannot complete this step as it is assigned to user clx...",
  "statusCode": 403
}

Best Practices

💡 Tips

1. Workflow Design

  • • Use PARALLEL_GATEWAY for independent approvals (faster)
  • • Set auto-approve for low-risk approvals (< €5k)
  • • Use CONDITIONAL_BRANCH instead of multiple workflows
  • • Always configure escalation (prevents FAILED status)

2. Performance

  • • Keep workflow.data < 500KB (limit: 1MB)
  • • Use webhook timeouts (default: 10s, max: 60s)
  • • Avoid deeply nested conditions (max: 10 levels)

3. Security

  • • Never store secrets in workflow data (trigger, variables, stepOutputs); webhook credentials belong in the step configuration of the template, which is visible only with workflows.viewTemplates
  • • Validate webhook URLs (SSRF protection active)
  • • Rotate API keys for external triggers (every 90 days)
  • • Check permissions before template publish

4. Monitoring

  • • Use activity log for audit trail
  • • Monitor for FAILED/PAUSED workflows
  • • SLA monitoring for time-critical workflows

Technical Details

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      FRONTEND (React)                        │
│  • Visual Workflow Designer (React Flow)                   │
│  • Instance Monitoring Dashboard                           │
│  • Step Action UI (Complete, Approve, Form)                │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    BACKEND API (Node.js)                     │
│  • /api/workflow/instances (CRUD)                          │
│  • /api/workflow/catalog (Browse & Start)                  │
│  • /api/workflow/api/trigger (External Systems)            │
│  • Validation (Zod), RBAC, Activity Logging                │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│              WORKFLOW-ENGINE (Separate Container)            │
│  • WorkflowEngine: State Machine, Step Orchestration       │
│  • StepExecutorFactory: Routes to correct Executor         │
│  • 8 Step Executors (Manual, Approval, Automated, etc.)    │
│  • ConditionEvaluator: 18 Operators                        │
│  • AutoApprovalService: Field/Permission/Role Logic        │
│  • TimerChecker: Polls Timer Jobs every 30s                │
│  • Redis PubSub: workflow:step:complete Events             │
└─────────────────────────────────────────────────────────────┘
                              │
                   ┌──────────┴──────────┐
                   ▼                     ▼
         ┌─────────────────┐   ┌─────────────────┐
         │  NOTIFICATION   │   │  JOB-WORKER    │
         │  WORKER         │   │  (CronJobs)     │
         │                 │   │                 │
         │  • Email        │   │  • Timer Jobs   │
         │  • In-App       │   │  • Scheduled    │
         └─────────────────┘   └─────────────────┘

Versioning

Workflows support versioning at template level.

Note: Running instances always use the template version they were started with. Template changes only affect new instances.

Attachments

Workflows use the Unified Attachment System for approval documents, supporting documents, etc.:

# Upload file to workflow
POST /api/attachments/WORKFLOW/:workflowId

# All attachments of a workflow
GET /api/attachments/WORKFLOW/:workflowId
Details: See Attachments & File Settings API for zero-trust virus scan, file settings and retention policies.