Human Tasks
Human tasks (userTask) pause process execution until a user manually completes them. They are the primary mechanism for involving people in automated workflows.
Task Lifecycle
┌─────────┐ claim ┌─────────┐ complete ┌───────────┐
│ pending │──────────→ │ claimed │──────────────→│ completed │
└────┬────┘ └────┬────┘ └───────────┘
│ ↑ cancel ↙
│ └──────────────┘
│ unclaim /
│ delegate
│ cancel
↓
┌───────────┐
│ cancelled │
└───────────┘
| Status | Description |
|---|---|
pending | Task is waiting; no one has claimed it yet |
claimed | A user has taken ownership |
completed | Task is done; process continues to the next element |
cancelled | Task was cancelled (e.g., the process instance was terminated) |
Defining a Human Task in a Process
{
"id": "review-expense",
"type": "userTask",
"name": "Review Expense Report",
"assignee": "{{variables.managerId}}",
"outgoing": ["flow-to-decision"]
}
The process pauses at review-expense and waits for an external API call to complete it. The assignee field supports variable interpolation; if omitted, the task goes into the unassigned pool.
A userTask may also declare candidateGroups (an array of group names) to make the task visible to multiple users:
{
"id": "legal-review",
"type": "userTask",
"name": "Legal Review",
"candidateGroups": ["legal", "compliance"],
"outgoing": ["flow-approved"]
}
candidateGroups is stored as metadata and returned in task responses, but the API does not enforce group membership at claim or delegate time. Any authenticated client can claim any task for any assignee. Use candidateGroups in your application to control which tasks are shown to which users — your application is responsible for checking group membership before calling the claim endpoint.
There is no query filter for ?candidateGroup= in GET /tasks today.
Listing Tasks
Request:
curl https://api.stateway.io/v1/tasks \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": [
{
"id": "task_01j...",
"name": "Review Expense Report",
"status": "pending",
"instance_id": "inst_01j...",
"element_id": "review-expense",
"assignee": "alice@acme.com",
"variables": {
"amount": 1500,
"requester": "john@acme.com"
},
"created_at": "2026-04-26T12:00:00.000Z",
"claimed_at": null,
"completed_at": null,
"due_at": null
}
]
}
Filters
# By status
curl "https://api.stateway.io/v1/tasks?status=pending" \
-H "X-API-Key: sw_live_your_key"
# By assignee
curl "https://api.stateway.io/v1/tasks?assignee=alice@acme.com" \
-H "X-API-Key: sw_live_your_key"
# By process instance
curl "https://api.stateway.io/v1/tasks?instance_id=inst_01j..." \
-H "X-API-Key: sw_live_your_key"
Getting Task Details
Request:
curl https://api.stateway.io/v1/tasks/{task_id} \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": {
"id": "task_01j...",
"name": "Review Expense Report",
"status": "pending",
"instance_id": "inst_01j...",
"element_id": "review-expense",
"assignee": null,
"variables": {
"amount": 1500,
"requester": "john@acme.com"
},
"created_at": "2026-04-26T12:00:00.000Z",
"claimed_at": null,
"completed_at": null,
"due_at": null
}
}
Claiming a Task
Take ownership of a pending task:
Request:
curl -X POST https://api.stateway.io/v1/tasks/{task_id}/claim \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "assignee": "alice@acme.com" }'
Response:
{
"data": {
"id": "task_01j...",
"name": "Review Expense Report",
"status": "claimed",
"instance_id": "inst_01j...",
"assignee": "alice@acme.com",
"claimed_at": "2026-04-26T12:05:00.000Z",
"completed_at": null
}
}
Only tasks with status pending can be claimed. Claiming an already-claimed task returns 404 NOT_FOUND.
The API does not validate that the assignee belongs to the task's candidateGroups. Enforce group membership in your application before calling this endpoint.
Completing a Task
Provide output variables and advance the process:
Request:
curl -X POST https://api.stateway.io/v1/tasks/{task_id}/complete \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"variables": {
"approved": true,
"approvedAmount": 1500,
"comments": "Approved — valid business expense"
}
}'
Response:
{
"data": {
"id": "task_01j...",
"status": "completed"
}
}
The engine merges the provided variables into the process instance and resumes execution at the next element.
| Response | Meaning |
|---|---|
200 | Task completed, process resumed |
400 VALIDATION_ERROR | Three distinct causes. (1) The submitted variables is not a plain object. (2) The merged payload would exceed the variables size cap (MAX_VARIABLES_PAYLOAD_BYTES) — for (1)/(2), nothing is mutated in the common case; in the rare case where the size check only fails after a concurrent write grows the instance mid-request, human_tasks is reverted back to its pre-request state before the error is returned, so this endpoint never leaves the task marked completed for a request that didn't actually reach the process instance. (3) The submitted outputs fail the task's declared activityContract output validation — unlike (1)/(2), this cause does mutate state before returning the error: the task is cancelled (if a matching interrupting error boundary event exists on this task) or the process instance is marked error (otherwise). |
409 CONFLICT | Two distinct causes: (1) the task's underlying execution token was already dead (for example, the process instance had already errored for an unrelated reason). The completion was not applied — human_tasks and process_instances are left unchanged. The instance needs recovery (e.g. via token move) before the task can be completed; do not retry with the same request. (2) another concurrent request already completed (or cancelled) this same task and won the race. The task is already in its final state and the instance is healthy — do not attempt token-move recovery in this case. Re-fetch the task (GET /tasks/{task_id}) to see the outcome instead of retrying; this endpoint is not idempotent, so blindly retrying the same request will keep failing. |
Cancelling a Task
Cancel a task without advancing the process:
Request:
curl -X POST https://api.stateway.io/v1/tasks/{task_id}/cancel \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": {
"id": "task_01j...",
"name": "Review Expense Report",
"status": "cancelled",
"instance_id": "inst_01j...",
"completed_at": "2026-06-07T10:00:00.000Z"
}
}
The task is marked as cancelled. The process is not advanced — the instance remains in its current state.
This endpoint is idempotent: cancelling an already-cancelled task returns 200 with the current task data.
| Task status | Behaviour |
|---|---|
pending | Cancelled → 200 |
claimed | Cancelled → 200 |
cancelled | No-op → 200 |
completed | Cannot cancel → 409 CONFLICT |
Use cancel to clean up tasks that are no longer relevant — for example, after terminating a process instance or when a business event makes the task obsolete. Cancelled tasks are retained in the system and visible via GET /tasks?status=cancelled.
Unclaiming a Task
Release a claimed task back to the pool:
Request:
curl -X POST https://api.stateway.io/v1/tasks/{task_id}/unclaim \
-H "X-API-Key: sw_live_your_key"
Response:
{
"data": {
"id": "task_01j...",
"status": "pending",
"assignee": null,
"claimed_at": null
}
}
The task returns to pending status and assignee is cleared.
Delegating a Task
Reassign a task to a different user:
Request:
curl -X POST https://api.stateway.io/v1/tasks/{task_id}/delegate \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "assignee": "bob@acme.com" }'
Response:
{
"data": {
"id": "task_01j...",
"status": "pending",
"assignee": "bob@acme.com",
"claimed_at": null
}
}
The task is reassigned to bob@acme.com and returns to pending status (the new assignee must claim it before completing).
Task Variables
Tasks have access to the process instance variables at the time they were created. These serve as input context. When completing a task, you can provide output variables that are merged back into the instance.
Process variables (at task creation):
{ amount: 1500, requester: "john@acme.com" }
│
▼
┌────────────────┐
│ userTask │ ← reads amount, requester
│ Review │
└────────┬───────┘
│ complete({ approved: true })
▼
Process variables (after completion):
{ amount: 1500, requester: "john@acme.com", approved: true }
Patterns
These patterns show common process structures involving human tasks. Use them as building blocks when designing your processes.
Sequential Approval
{
"elements": [
{ "id": "manager-review", "type": "userTask", "name": "Manager Review", "outgoing": ["flow-to-director"] },
{ "id": "director-review", "type": "userTask", "name": "Director Review", "outgoing": ["flow-to-end"] }
],
"flows": [
{ "id": "flow-to-director", "sourceRef": "manager-review", "targetRef": "director-review" },
{ "id": "flow-to-end", "sourceRef": "director-review", "targetRef": "end" }
]
}
Conditional Routing After Task
{
"elements": [
{ "id": "review", "type": "userTask", "name": "Review", "outgoing": ["flow-to-gw"] },
{ "id": "gw", "type": "exclusiveGateway", "outgoing": ["flow-approve", "flow-reject"] },
{ "id": "end-ok", "type": "endEvent" },
{ "id": "end-fail", "type": "endEvent" }
],
"flows": [
{ "id": "flow-to-gw", "sourceRef": "review", "targetRef": "gw" },
{ "id": "flow-approve", "sourceRef": "gw", "targetRef": "end-ok", "condition": "{{variables.approved == true}}" },
{ "id": "flow-reject", "sourceRef": "gw", "targetRef": "end-fail", "condition": "{{variables.approved == false}}" }
]
}
Best Practices
- Claim before completing — ensures proper ownership tracking and prevents race conditions
- Use meaningful task names — these names surface in task lists and webhook payloads
- Include context in variables — provide all information a user needs to make a decision at instance start time
- Pair userTasks with timers — enforce SLAs by running a
timerEventin a parallel branch alongside the task (see SLA Enforcement in the Timers guide); boundary timer events are not supported yet (see Timers) - Filter by
assignee— build task inbox UIs usingGET /tasks?assignee=user@example.com - Subscribe to
task.overdue— use webhooks to notify assignees or trigger escalation flows