Skip to main content

User Tasks

User tasks are the human-handled work items in a process. The engine tracks the task's lifecycle, holds the variables it was created with, and waits for an external signal that the human has finished — typically delivered by a custom inbox UI or a workflow tool that calls the API.

This page is the integration guide for that external system. For modeling user tasks (assignment fields, forms, I/O mappings) see Tasks.

Lifecycle

A user task moves through one of four terminal states from CREATED:

StatusReached when
CREATEDThe activity entered the user-task node and the task was registered. The instance pauses until something terminates it
COMPLETEDAn external system calls Complete with the result variables
FAILEDAn external system calls ThrowError with a BPMN error code
CANCELEDThe instance was cancelled, an interrupting boundary fired on the task, the parent scope was modified, or the process terminated before the task finished

COMPLETED and FAILED carry the variables that were submitted alongside the call (completionVariables). CANCELED carries a best-effort cancelReason. FAILED carries the errorCode that was thrown.

Assignment model

A user task can be addressed three ways at the same time:

FieldNotes
assigneeA single user the task is currently assigned to. Empty when the task is in a candidate pool but not yet claimed
candidateUsersA list of users who may claim or complete the task even when there's no assignee
candidateGroupsIdentity-provider groups whose members may claim or complete the task

These fields come from <quantum:assignmentDefinition> in the model. They can be FEEL expressions evaluated at task entry, so the assignment can depend on the instance's variables.

Reassigning a CREATED task

Operators can change assignment after the task has been registered:

PATCH /projects/{projectID}/bpmn/user-tasks/{executionKey}
FieldNotes
assigneeNew assignee. Pass null to clear and leave only the candidate pool
candidateUsersReplacement list of candidate users
candidateGroupsReplacement list of candidate groups

The endpoint requires the Executor role on the project and only works on tasks still in CREATED. Tasks that have already terminated return 404.

Access control

The user-task endpoints have a dual-role model:

CallerCan list & act on
Has Executor (or higher) on the projectAny user task
Is the task's assigneeThis task
Is in candidateUsersThis task
Has any identity-provider group that overlaps candidateGroupsThis task

So the same Complete and ThrowError endpoints serve both an admin/integration use case (Executor service account) and an end-user use case (the human assignee).

The HTTP API

All endpoints live under the project: /projects/{projectID}/bpmn/user-tasks. Same bearer-token auth as the rest of the API — see Authentication.

List all user tasks (admin)

GET /projects/{projectID}/bpmn/user-tasks

Paginated. Optional query filters:

ParamNotes
workflowIDRestrict to one process instance
statusCREATED, COMPLETED, FAILED, or CANCELED
assigneeTasks currently assigned to this user
candidateUserTasks listing this user as a candidate
candidateGroupTasks listing this group
page, pageSizeStandard pagination

Requires Executor on the project. Useful for building an admin queue view.

List the caller's tasks

GET /projects/{projectID}/bpmn/user-tasks/me

Returns CREATED tasks the caller can act on — directly assigned, listed in candidateUsers, or whose candidateGroups overlap any of the caller's identity-provider groups. Requires Viewer on the project.

This is the right endpoint for an end-user "my inbox" UI. The platform doesn't ship one out of the box; the endpoint exists so you can build one.

Get a single user task

GET /projects/{projectID}/bpmn/user-tasks/{executionKey}

Fetches one task by execution key. Returns 403 if the caller is neither Executor nor on the task's assignment fields, 404 if the task doesn't exist.

A task object looks like this:

{
"id": "…",
"executionKey": "wf-abc:approve-request:1",
"workflowID": "wf-abc",
"nodeID": "approve-request",
"taskType": "UserTask",
"assignee": "manager",
"candidateGroups": ["approvers", "leads"],
"formKey": "approve-form",
"variables": { "request_id": "REQ-123", "amount": 500 },
"headers": { "priority": "high" },
"status": "CREATED",
"createdAt": "2026-05-02T12:34:56Z"
}

Complete a task

POST /projects/{projectID}/bpmn/user-tasks/{executionKey}/complete

Finalises the task successfully. The variables are merged into the originating instance's scope (after any output mappings declared on the user task) and the process advances.

FieldRequiredNotes
variablesnoOutput variables from the form / handler
curl -X POST "$API/projects/$PROJECT/bpmn/user-tasks/$KEY/complete" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variables": { "approved": true, "comment": "looks good" }
}'

Fail a task with a BPMN error

POST /projects/{projectID}/bpmn/user-tasks/{executionKey}/error

Reports that the task can't be completed. The errorCode is raised as a BPMN error from the user-task node — any matching error boundary event on the task takes the failure path, otherwise the instance gets an incident.

FieldRequiredNotes
errorCodeyesMatched against errorRef on attached error boundary events
variablesnoVariables submitted alongside the error. Available to error-boundary handlers
curl -X POST "$API/projects/$PROJECT/bpmn/user-tasks/$KEY/error" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"errorCode": "REJECTED",
"variables": { "reason": "missing supporting documents" }
}'

This is what makes error boundary events on user tasks useful — model the failure path in BPMN, route the rejection through it from your form's "reject" button.

Forms

User tasks carry two form references for your UI to resolve:

FieldSourceUse
formKey<quantum:formDefinition formKey="..."> in the modelStable string your UI maps to a rendering. Often the simplest pattern: the form key matches a known component or template name
formID<quantum:formDefinition formId="..."> in the modelResolved internal identifier when formKey corresponds to a stored form definition

Use whichever fits your stack. The engine doesn't render forms — it just hands the keys to your UI alongside the input variables and headers.

Error handling

The flow when a user task fails:

  1. The caller invokes ThrowError with an errorCode and optional variables.
  2. The engine raises a BPMN error from the user-task node carrying that code.
  3. If a matching error boundary event is attached to the task, the token routes through the boundary. The boundary handler's scope receives the variables submitted with the error.
  4. If there's no matching boundary, the error propagates up the scope chain to the nearest matching listener — an error boundary on a parent sub-process, or an error event sub-process.
  5. If nothing catches it, the instance gets an incident and pauses.

Treat ThrowError as the user-task analogue of a worker reporting a job failure — the BPMN error code is how the model expresses what went wrong.

In the UI

Each instance's detail page lists its user tasks under a dedicated accordion section. See Operating live instances → User tasks for the columns shown.

The diagram view of a running instance highlights any active user-task nodes alongside other active elements. There's no first-party inbox UI — the /me endpoint is there for you to build one.