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:
| Status | Reached when |
|---|---|
CREATED | The activity entered the user-task node and the task was registered. The instance pauses until something terminates it |
COMPLETED | An external system calls Complete with the result variables |
FAILED | An external system calls ThrowError with a BPMN error code |
CANCELED | The 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:
| Field | Notes |
|---|---|
assignee | A single user the task is currently assigned to. Empty when the task is in a candidate pool but not yet claimed |
candidateUsers | A list of users who may claim or complete the task even when there's no assignee |
candidateGroups | Identity-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}
| Field | Notes |
|---|---|
assignee | New assignee. Pass null to clear and leave only the candidate pool |
candidateUsers | Replacement list of candidate users |
candidateGroups | Replacement 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:
| Caller | Can list & act on |
|---|---|
| Has Executor (or higher) on the project | Any user task |
Is the task's assignee | This task |
Is in candidateUsers | This task |
Has any identity-provider group that overlaps candidateGroups | This 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:
| Param | Notes |
|---|---|
workflowID | Restrict to one process instance |
status | CREATED, COMPLETED, FAILED, or CANCELED |
assignee | Tasks currently assigned to this user |
candidateUser | Tasks listing this user as a candidate |
candidateGroup | Tasks listing this group |
page, pageSize | Standard 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.
| Field | Required | Notes |
|---|---|---|
variables | no | Output 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.
| Field | Required | Notes |
|---|---|---|
errorCode | yes | Matched against errorRef on attached error boundary events |
variables | no | Variables 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:
| Field | Source | Use |
|---|---|---|
formKey | <quantum:formDefinition formKey="..."> in the model | Stable 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 model | Resolved 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:
- The caller invokes ThrowError with an
errorCodeand optional variables. - The engine raises a BPMN error from the user-task node carrying that code.
- 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.
- 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.
- 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.