REST API
All paths below are relative to the API base URL (https://localhost:7140 in the Aspire dev stack). Endpoints are grouped by controller: /Definitions/* (deploy, disable, enable, list), /Execution/* (start, message, signal, complete-activity, fail-activity, evaluate-conditions), /Instances/* (state, purge), /UserTasks/* and /Deployments/* (deployment requests). The old /Workflow/* prefix was removed when the controller was split (see PR #614) and now returns 404.
| Endpoint | Method | Body |
|---|---|---|
/Definitions/deploy | POST | {"BpmnXml":"<raw BPMN XML string>"} |
/Execution/start | POST | {"WorkflowId":"process-id"} or {"WorkflowId":"process-id","Variables":{"key":"value"},"BusinessId":"order-123"} — BusinessId is optional (see Business id); Variables is optional; when provided, the variables are merged into the root scope before the workflow starts (required for message event sub-processes that resolve correlation keys from variables at scope entry) |
/Execution/message | POST | {"MessageName":"...", "CorrelationKey":"...", "Variables":{}, "BusinessId":"...", "IdempotencyKey":"..."} — BusinessId is optional and applies only to instances a message start event creates; a message delivered to a running instance leaves that instance’s business id unchanged. IdempotencyKey is optional and makes the request safe to retry — see Retrying a message |
/Execution/signal | POST | {"SignalName":"..."} — not deduplicated: a retry starts signal-start processes again |
/Execution/complete-activity | POST | {"WorkflowInstanceId":"guid", "ActivityId":"activity-id", "Variables":{}, "ActivityInstanceId":"guid"} — ActivityInstanceId is optional; with it a repeated completion is ignored (external workers, see External workers (gRPC)) |
/Execution/fail-activity | POST | {"WorkflowInstanceId":"guid", "ActivityId":"activity-id", "ActivityInstanceId":"guid", "ErrorCode":"PAYMENT_DECLINED", "ErrorMessage":"...", "Variables":{}} — fails the activity with a BPMN error; routes via an error boundary event if one matches |
/Execution/{workflowInstanceId}/activities/{activityInstanceId}/variables | GET | (none) — The variables an active activity instance sees, read from the instance itself; 404 once it is no longer active. For workers holding a deferred task |
/Execution/evaluate-conditions | POST | {"WorkflowId":"process-id", "Variables":{"key":"value"}} — Evaluates all conditional start events (or only those for the given WorkflowId if provided) against the supplied variables. Returns {"StartedInstanceIds":["guid",...], "Errors":["..."]}. Errors is present only when one or more listeners failed during evaluation. |
/Instances/{instanceId}/state | GET | (none) — Returns the current state snapshot for a specific workflow instance |
/Instances/purge | POST | {"CompletedBefore":"2026-07-01T00:00:00Z", "BatchSize":500, "MaxInstances":10000} — Deletes instances that finished before the cutoff. Only CompletedBefore is required. See Purge endpoint. |
/UserTasks | GET | (query string) — Paginated list of pending user tasks. See User Task endpoints. |
/UserTasks/{activityInstanceId} | GET | (none) — Single user-task lookup. |
/UserTasks/{activityInstanceId}/claim | POST | {"UserId":"alice"} |
/UserTasks/{activityInstanceId}/unclaim | POST | (empty body) |
/UserTasks/{activityInstanceId}/complete | POST | {"UserId":"alice", "Variables":{"approved":true}} |
/UserTasks/{activityInstanceId}/fail | POST | {"errorMessage":"reason", "errorCode":"400"} — fails the task; routes via Error Boundary Event if one matches. |
/UserTasks/{activityInstanceId}/cancel | POST | {"reason":"optional"} — cancels the task; no error propagation. Idempotent. |
/Deployments/requests | POST | {"BpmnXml":"...", "Comment":"...", "DeployDisabled":false, "RequestedBy":"alice"} — a deploy that needs approvals. See Deployment request endpoints. |
/Deployments/requests | GET | ?status=Pending (optional) — newest first, without the BPMN. |
/Deployments/requests/{id} | GET | (none) — one request. …/{id}/bpmn returns its BPMN XML. |
/Deployments/requests/{id}/approve | POST | {"UserId":"bob", "Comment":"optional", "UserGroups":["release-managers"]} — UserGroups only when auth is off |
/Deployments/requests/{id}/reject | POST | {"UserId":"bob", "Reason":"required", "UserGroups":["release-managers"]} |
/Definitions/latest | GET | ?keys=order-process&keys=invoice (up to 100) — each process’s latest version with bpmnSha256, a SHA-256 of its BPMN XML. Used by other environments’ environments board. |
/Deployments/requests/{id}/withdraw | POST | {"UserId":"alice"} — requester only. |
/Deployments/requests/{id}/retry | POST | (empty body) — deploys an approved request whose deploy failed. |
Endpoint details
Section titled “Endpoint details”POST /Definitions/deploy
Section titled “POST /Definitions/deploy”Deploys a BPMN process definition to the engine. The request body contains the raw BPMN XML as a string. On success the engine parses the XML, registers the process definition, and returns the assigned key and version number. If the same process ID is deployed again, the version is incremented automatically.
Why this endpoint exists: Before any workflow instance can be started, its process definition must be deployed. This endpoint is the programmatic entry point for uploading BPMN definitions — the Web UI also uses it internally when you import a .bpmn file.
Request
POST /Definitions/deployContent-Type: application/json
{ "BpmnXml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><definitions ...>...</definitions>"}Success response (200)
{ "ProcessDefinitionKey": "my-process", "Version": 1}Error response (400) — returned when BpmnXml is empty, is not well-formed XML, or is not a deployable BPMN model (e.g. no <process> element). The message carries the parser diagnostic:
{ "error": "Invalid BPMN: BPMN file must contain a process element"}The same 400 rejects a definition with a condition or script that does not compile — sequence-flow conditions, complex-gateway activation and multi-instance completion conditions, conditional events and script tasks are all compiled at deploy. The message names each element and carries the parser diagnostic, and no version is stored:
{ "error": "Invalid BPMN: Process 'order-process' has invalid expressions: script task 'Task_total': ')' or operator expected (at index 18)."}Best-practice example
# Deploy a local BPMN file via curlBPMN_XML=$(cat my-workflow.bpmn | jq -Rs .)curl -s -X POST https://localhost:7140/Definitions/deploy \ -H "Content-Type: application/json" \ -d "{\"BpmnXml\": $BPMN_XML}"This reads the .bpmn file, JSON-escapes it with jq -Rs, and sends it to the deploy endpoint. The response contains the ProcessDefinitionKey you pass to /start to create instances.
Conflict (409) — returned when the cluster requires deploy approvals (Fleans__Deployments__RequiredApprovals above 0). Submit a deployment request instead.
Deployment request endpoints
Section titled “Deployment request endpoints”A deployment request is a deploy that waits for Fleans__Deployments__RequiredApprovals approvals from distinct users other than the requester, then deploys itself as the next version. With 0 required, POST /Deployments/requests deploys at once — that is how another environment’s Admin UI promotes a version here. See the Deploy approvals guide for the workflow.
Identity. RequestedBy / UserId follow the user-task rule: required when authentication is off; under JWT auth the user comes from the token, and a body value that differs from it is rejected with 403. UserGroups follows the candidateGroups rule: read from the body when authentication is off, from the token’s Authentication:GroupsClaim under JWT (body ignored). They are checked against the request’s reviewerGroups (Fleans__Deployments__ReviewerGroups).
Submit
POST /Deployments/requestsContent-Type: application/json
{ "BpmnXml": "<?xml version=\"1.0\"?><definitions ...>...</definitions>", "Comment": "Adds an escalation timer to the review task", "DeployDisabled": false, "RequestedBy": "alice"}Response (201) — the request, also returned by every action below:
{ "id": "3f0c…", "processDefinitionKey": "order-process", "requestedBy": "alice", "requestedAt": "2026-10-23T12:00:00+00:00", "comment": "Adds an escalation timer to the review task", "requiredApprovals": 2, "approvals": [], "status": "Pending", "deployDisabled": false, "closedBy": null, "closeReason": null, "deployedVersion": null, "deployError": null, "reviewerGroups": ["release-managers"]}status is Pending, Approved (approved; the deploy failed and deployError says why — POST …/retry), Deployed (deployedVersion set), Rejected or Withdrawn.
| Status | When |
|---|---|
400 | Empty or invalid BPMN, missing UserId (auth off), reject without Reason. |
403 | Body UserId differs from the token user. |
404 | Unknown request id. |
409 | The transition isn’t allowed: the requester approving or rejecting their own request, a second approval by the same user, a reviewer outside reviewerGroups, or acting on a request that is no longer pending. |
POST /Execution/start
Section titled “POST /Execution/start”Starts a new workflow instance from a deployed process definition. Returns the instance ID which can be used to track state, send messages, or complete activities.
Request
POST /Execution/startContent-Type: application/json
{ "WorkflowId": "my-process", "Variables": { "amount": 100 }, "BusinessId": "order-123"}Variables is optional. When provided, variables are merged into the root scope before the workflow starts.
BusinessId is optional. It is a business identifier for the instance, such as an order number or a contract id. See Business id.
Success response (200)
{ "WorkflowInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"}Error responses
- 400 —
{"error": "WorkflowId is required"}, or an invalidBusinessId(ProblemDetails; empty or whitespace, leading or trailing whitespace, control characters such as CR/LF or tab, or longer than 256 characters) - 404 — the process key was never deployed (ProblemDetails,
detail:Workflow with id 'my-process' is not registered. …) - 409 — the process key is disabled (ProblemDetails,
detail:Process 'my-process' is disabled. Enable it before creating new instances.)
Disabled processes
Section titled “Disabled processes”POST /Definitions/disable stops a process key from starting new instances, whatever would start them:
POST /Execution/startreturns 409. The admin UI’s start of a specific version is refused the same way.- Message, signal and conditional start events start nothing for that process. The other processes listening for the same message or signal still start.
- A timer start event’s scheduler is deactivated. If its reminder fires anyway, it starts nothing.
- A call activity that calls the disabled process fails, like any other child-start failure. An error boundary on the call activity catches it.
Instances that are already running keep running. POST /Definitions/enable re-registers the start events.
Every silo and API host keeps its own list of which processes a message, signal or conditional start event starts. A deploy, enable or disable updates every host’s list before the call returns, so the next message or signal sent to any host sees the change. A host that could not be reached at that moment catches up within 30 seconds. Disable takes effect at once in every case: each start checks that the process is enabled.
Business id
Section titled “Business id”A process instance can have a business id: a value your systems already know, such as an order number. Use it to find “the process for order 123” without storing the generated instance GUID. It follows Camunda 8.9+‘s businessId:
- Set only at start, on
POST /Execution/start, onPOST /Execution/messagewhen a message start event creates the instance, or in the admin UI’s Business ID for Start field. It can’t be changed afterwards: a message correlated to a running instance does not change it. Instances started by timers, signals, conditional starts or call activities have none. - At most 256 characters, and not empty or whitespace. An invalid value returns 400.
- Stored exactly as sent. Leading or trailing whitespace is rejected with
400rather than trimmed, so the value you look up is the value you started with. - Not unique. Two instances can share a business id. Fleans does not enforce uniqueness, so filter by it and pick the instance you need.
- Not inherited. A call activity’s child instance does not get the parent’s business id.
- Visible everywhere: as
businessIdonGET /Instances/{id}/stateand on the instance listings. Find instances by it with thebusinessIdquery parameter, for example/Definitions/my-process/instances?businessId=order-123. It matches exactly, so values with,or|work too (URL-encode them); those characters would split a Sievefiltersterm. A padded, too-long or control-character value returns400; an empty value is ignored (no filter). The MCPListInstancestool takes the samebusinessIdargument. It also appears in the admin UI, and custom-task plugins get it asCustomTaskExecutionContext.BusinessId.
POST /Execution/message
Section titled “POST /Execution/message”Delivers a message to workflow instances waiting for it, correlated by key. Used to trigger intermediate message catch events and message start events.
Request
POST /Execution/messageContent-Type: application/json
{ "MessageName": "payment-received", "CorrelationKey": "order-123", "Variables": { "paymentId": "pay-456" }}Success response (200)
{ "Delivered": true, "WorkflowInstanceIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]}Error responses
- 400 —
{"error": "MessageName is required"}, or an invalidBusinessId(ProblemDetails) - 400 — a blank
IdempotencyKey, or one that makesMessageName+/+ the URL-escaped key longer than 1024 characters (ProblemDetails) - 404 —
{"error": "No active subscription found for message 'payment-received' with correlation key 'order-123'"}— no workflow instance is currently waiting for this message/key combination
Retrying a message
Section titled “Retrying a message”Without an IdempotencyKey, a retried request is a second message: if the first one started instances through a message start event and only its response was lost, the retry starts them again. Send an IdempotencyKey — a value unique to this message, such as an event id or a request id — and the request can be retried safely:
POST /Execution/messageContent-Type: application/json
{ "MessageName": "order-placed", "Variables": { "orderId": "order-123" }, "IdempotencyKey": "evt-8f14e45f"}- A repeat does nothing and returns the first outcome. A request with the same
MessageNameandIdempotencyKeywithin 24 hours of the first returns its response: the sameWorkflowInstanceIdswhen it started instances, or"Delivered": truewhen it was delivered to a waiting instance. It does not start, correlate or merge variables again. The first request wins: a repeat with otherVariables,CorrelationKeyorBusinessIdstill gets the first outcome. - Concurrent repeats are safe. Requests with one key run one at a time; the others wait and get the first outcome.
- An interrupted request resumes. The instance ids are recorded before the first instance starts, so a retry after a crash or timeout starts the same instances, not new ones.
- A request that delivered nothing is not recorded (404, or every subscribed process disabled): a retry is tried again, for example after you deploy or enable the process.
- The key is scoped to the message name. The same
IdempotencyKeyon another message name is another message. - Only requests with a key pay for it: one extra grain call and one or two writes to the
MessageDeduplicationstable. After 24 hours the key starts afresh. Expired rows are not deleted yet. - Signals are not deduplicated. A signal is a broadcast with no identity, so
POST /Execution/signaltakes noIdempotencyKey. When a start must happen once, use a message start event with a key.
POST /Execution/signal
Section titled “POST /Execution/signal”Broadcasts a signal to all workflow instances listening for it. Unlike messages, signals have no correlation key — every matching listener receives the signal.
Request
POST /Execution/signalContent-Type: application/json
{ "SignalName": "global-alert"}Success response (200)
{ "DeliveredCount": 2, "WorkflowInstanceIds": [ "3fa85f64-5717-4562-b3fc-2c963f66afa6", "8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8" ]}Error responses
- 400 —
{"error": "SignalName is required"} - 404 —
{"error": "No active subscription found for signal 'global-alert'"}— no workflow instance is currently listening for this signal
DeliveredCount counts the waiting instances the signal was queued for. The call returns once the deliveries are queued; the instances receive the signal moments later, at the pace set by Fleans:Signals:DeliveryConcurrency (see Configuration).
POST /Execution/complete-activity
Section titled “POST /Execution/complete-activity”Completes a manual activity (e.g., a task waiting for external input) on a running workflow instance, optionally passing output variables.
Request
POST /Execution/complete-activityContent-Type: application/json
{ "WorkflowInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "ActivityId": "review-task", "Variables": { "approved": true }}Success response (200) — empty body
Error responses
- 400 —
{"error": "WorkflowInstanceId is required"}/{"error": "ActivityId is required"} - 404 — no workflow instance with that id (ProblemDetails,
detail:Workflow instance '<id>' not found.) - 409 — the activity has no active entry: unknown activity id, or an activity that already completed (ProblemDetails,
detail:No active entry found for activity 'review-task'.). Also returned when the activity is a user task — use the/UserTasks/{id}/completeendpoint instead. - 503 — the instance is briefly unavailable after its journal write conflicted with another writer (ProblemDetails; log 1137). Nothing of the call was applied: retry it.
With ActivityInstanceId — for example the activity_instance_id of a task an external worker received — the call completes exactly that activity instance and answers 200 without doing anything when it has already completed, failed or been cancelled. A worker that got the same task twice, or retries after a timeout, can therefore send the completion again. Without it, the active instance of ActivityId completes and a repeat is a 409.
POST /Execution/fail-activity
Section titled “POST /Execution/fail-activity”Fails one activity instance with a BPMN error, for external workers whose work failed. ErrorCode is matched by error boundary events and error event sub-processes; with no match the instance fails. Variables (optional) are merged into the scope of whatever catches the error. Like a completion with ActivityInstanceId, a failure of an activity instance that has already finished is ignored.
POST /Execution/fail-activityContent-Type: application/json
{ "WorkflowInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "ActivityId": "charge-payment", "ActivityInstanceId": "9b2c4e1a-6f3d-4b8e-a1c2-7d5e9f0a1b2c", "ErrorCode": "PAYMENT_DECLINED", "ErrorMessage": "card declined", "Variables": { "declineReason": "insufficient-funds" }}Success response (200) — empty body
Error responses
- 400 —
WorkflowInstanceId,ActivityId,ActivityInstanceIdorErrorCodemissing - 404 — no workflow instance with that id
Read endpoints
Section titled “Read endpoints”Paginated read endpoints accept page (default 1), pageSize (default 20, max 100), and Sieve sorts / filters query parameters, and return {"items": [...], "totalCount": n, "page": p, "pageSize": s}. An unknown key or version returns an empty page (200), not 404.
| Verb | Path | Returns |
|---|---|---|
| GET | /Definitions | Deployed process-definition versions (processDefinitionId, processDefinitionKey, version, deployedAt, activitiesCount, sequenceFlowsCount, isActive). Filterable on ProcessDefinitionKey, Version, IsActive; sortable on ProcessDefinitionKey, Version, DeployedAt. |
| GET | /Definitions/{key}/instances | Instances of every version of key (instanceId, processDefinitionId, isStarted, isCompleted, isCancelled, timestamps). Filterable on IsStarted, IsCompleted, IsCancelled, IsFailed, BusinessId, CreatedAt; businessId=<value> is an exact-match filter. Each row also carries businessId (or null) and stateVersion, the event-log version the row reflects. |
| GET | /Definitions/{key}/{version}/instances | Same, restricted to one version. |
| GET | /custom-tasks | Custom-task plugin catalog: one entry per task type with displayName, parameterSchema, the siloNames hosting it, category (or null), visibility (Public or Private) and allowedProcessKeys (empty for a public plugin). A private plugin whose hosts are gone is listed with no siloNames. Not paginated. |
| GET | /custom-tasks/{taskType} | One catalog entry; 404 if no plugin registers that task type. |
| PUT | /custom-tasks/{taskType}/allowed-processes | Replaces a private plugin’s allow-list with the body { "processKeys": ["billing"] } and returns the entry. 400 on a blank key, 404 if no plugin registers that task type, 409 if the plugin is public. Later deployments are checked against the new list; running instances are not affected. |
curl -k 'https://localhost:7140/Definitions?sorts=-Version&filters=ProcessDefinitionKey==my-process'curl -k 'https://localhost:7140/Definitions/my-process/2/instances?filters=IsCompleted==true'curl -k 'https://localhost:7140/custom-tasks/rest-call' # 404 unless a host runs the REST Caller pluginUser Task endpoints
Section titled “User Task endpoints”User-task endpoints expose the human-in-the-loop lifecycle of <bpmn:userTask> activities. The conceptual model (states, who-can-claim, expected outputs) lives in the User Tasks guide — this section is the authoritative wire reference.
User Task operations summary
| Verb | Path | Surface | Auth | Success |
|---|---|---|---|---|
| GET | /UserTasks | Query | optional | 200 OK |
| GET | /UserTasks/{activityInstanceId} | Query | optional | 200 OK |
| POST | /UserTasks/{activityInstanceId}/claim | Mutation | UserId from body (no auth) or token (JWT) | 200 OK |
| POST | /UserTasks/{activityInstanceId}/unclaim | Mutation | NONE — see below | 200 OK |
| POST | /UserTasks/{activityInstanceId}/complete | Mutation | UserId from body (no auth) or token (JWT) | 200 OK |
| POST | /UserTasks/{activityInstanceId}/fail | Mutation | ErrorMessage required in body | 200 OK |
| POST | /UserTasks/{activityInstanceId}/cancel | Mutation | body optional | 200 OK |
Auth above refers to API-level JWT bearer auth, which is opt-in for the entire API — see Authentication. With auth disabled, the UserId field in claim/complete bodies is trusted as the acting user. With JWT auth enabled, the acting user comes from the token’s Authentication:UserIdClaim claim. The body UserId is then optional, and a value that differs from the token is rejected with 403.
Error response shapes
Section titled “Error response shapes”The User Task endpoints emit two distinct error wire shapes depending on which layer rejects the request:
| Status | Surface | Wire shape |
|---|---|---|
400 (model-binding) | ASP.NET auto via [ApiController] + AddProblemDetails() | application/problem+json (RFC 7807) |
400 (controller-emitted, e.g. UserId is required) | Custom ErrorResponse | {"error":"..."} |
404 (task not found) | Custom ErrorResponse | {"error":"..."} |
409 (wrong claimer / missing required outputs) | Custom ErrorResponse | {"error":"..."} (the two cases discriminate only by message text — see the /complete section below) |
5xx (unhandled exception) | GlobalExceptionHandler ProblemDetails | application/problem+json (RFC 7807) |
Property casing in the controller-emitted {"error":"..."} shape is camelCase — the Fleans API serializes via the System.Text.Json default policy.
GET /UserTasks
Section titled “GET /UserTasks”Lists pending user tasks across all running workflow instances, with optional filters and standard pagination/sort/filter query parameters.
Request
GET /UserTasks?assignee=alice&candidateGroup=approvers&page=1&pageSize=20&sorts=&filters=| Query param | Type | Description |
|---|---|---|
assignee | string? | Restrict to tasks assigned to this user (matches Assignee field). |
candidateGroup | string? | Restrict to tasks where this group is a candidate. |
page | int (default 1) | 1-based page index. |
pageSize | int (default 20) | Page size. |
sorts | string? | Sieve-style sort expression (e.g. createdAt, -createdAt). |
filters | string? | Sieve-style filter expression. |
Success response (200) — paginated envelope with a data: UserTaskResponse[] payload:
{ "data": [ { "workflowInstanceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "activityInstanceId": "8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8", "activityId": "review-task", "assignee": "alice", "candidateGroups": ["approvers"], "candidateUsers": [], "claimedBy": null, "taskState": "Created", "createdAt": "2026-05-03T10:30:00+00:00", "expectedOutputVariables": ["approved"] } ], "page": 1, "pageSize": 20, "totalCount": 1}Curl example
curl -k "https://localhost:7140/UserTasks?assignee=alice&page=1&pageSize=20"GET /UserTasks/{activityInstanceId}
Section titled “GET /UserTasks/{activityInstanceId}”Returns a single user task by its activity-instance id, or 404 if the id is unknown / no longer pending.
Request
GET /UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8Success response (200) — single UserTaskResponse (same shape as the array element above).
Error response (404)
{ "error": "User task '8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8' not found" }Curl example
curl -k https://localhost:7140/UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8POST /UserTasks/{activityInstanceId}/claim
Section titled “POST /UserTasks/{activityInstanceId}/claim”Claims a pending task for UserId. Subsequent claims by a different user overwrite the claim — Fleans does not enforce first-claim-wins (see the User Tasks guide for the lifecycle table).
Request
POST /UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/claimContent-Type: application/json
{ "UserId": "alice" }| Body field | Type | Required | Description |
|---|---|---|---|
UserId | string | yes without auth; optional under JWT | Acting user. Under JWT the token’s user is used, and a differing value returns 403. |
Success response (200) — empty body.
Error responses
- 400 —
{"error": "UserId is required"}— auth disabled and the body is missing orUserIdis empty/whitespace. - 403 — JWT auth only:
{"error": "UserId does not match the authenticated user"}when the bodyUserIddiffers from the token, or{"error": "Authenticated user has no user id claim"}when the token lacks the configured claim. - 404 —
{"error": "User task '<id>' not found"}— no pending task with that activity-instance id. - 409 — claim rejected by the domain layer (e.g. caller is not in
Assignee/CandidateUsers/CandidateGroups); the body is the underlyingInvalidOperationExceptionmessage.
Curl example
curl -k -X POST https://localhost:7140/UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/claim \ -H "Content-Type: application/json" \ -d '{"UserId":"alice"}'POST /UserTasks/{activityInstanceId}/unclaim
Section titled “POST /UserTasks/{activityInstanceId}/unclaim”Releases an existing claim so another user can claim the task. The body is empty.
Request
POST /UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/unclaimContent-Type: application/jsonSuccess response (200) — empty body. Note: succeeds whether the task was previously claimed or not.
Error response (404)
{ "error": "User task '8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8' not found" }Curl example
curl -k -X POST https://localhost:7140/UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/unclaim \ -H "Content-Type: application/json"POST /UserTasks/{activityInstanceId}/complete
Section titled “POST /UserTasks/{activityInstanceId}/complete”Completes the task on behalf of UserId, merging Variables into the enclosing scope and advancing the token. The caller must be the current claimer, and all variables declared in <fleans:expectedOutputs> must be present.
Request
POST /UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/completeContent-Type: application/json
{ "UserId": "alice", "Variables": { "approved": true, "reviewerComment": "looks good" }}| Body field | Type | Required | Description |
|---|---|---|---|
UserId | string | yes without auth; optional under JWT | Acting user — must match claimedBy on the task. Under JWT the token’s user is used, and a differing value returns 403. |
Variables | object? | optional unless the task declares expectedOutputVariables | Output variables merged into the workflow’s enclosing scope. Every entry in expectedOutputVariables must have a value here. |
Success response (200) — empty body. The task is removed from the registry; subsequent GET /UserTasks/{id} returns 404.
Error responses
-
400 —
{"error": "UserId is required"}— auth disabled and noUserIdin the body. -
403 — JWT auth only: the body
UserIddiffers from the token, or the token lacks the configured user-id claim (same bodies asclaim). -
404 —
{"error": "User task '<id>' not found"}— already completed, never existed, or wrong id. -
409 — wrong claimer:
{ "error": "Task is claimed by bob, not alice" }Distinguishable by the
Task is claimed by …, not …prefix. -
409 — missing required outputs:
{ "error": "Missing required output variables: approved, reviewerComment" }Distinguishable by the
Missing required output variables:prefix. The list is the comma-joined set of declared<fleans:expectedOutputs>entries that are absent from the suppliedVariables.
Curl example
curl -k -X POST https://localhost:7140/UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/complete \ -H "Content-Type: application/json" \ -d '{"UserId":"alice","Variables":{"approved":true}}'POST /UserTasks/{activityInstanceId}/fail
Section titled “POST /UserTasks/{activityInstanceId}/fail”Fails a pending user task with an error code and message. The engine routes the failure through the standard FailActivity path — if an Error Boundary Event is attached to the task and its error code matches, the workflow continues via that boundary; otherwise the workflow instance enters a top-level error state.
Both operations are idempotent: calling them on a task that is already in a terminal state (Completed, Failed, or Cancelled) returns 200 OK without re-emitting events.
Request body
{ "errorMessage": "User rejected the task", "errorCode": "400"}| Field | Type | Required | Description |
|---|---|---|---|
errorMessage | string | Yes | Human-readable failure reason. |
errorCode | string | No (default "500") | Error code matched against Error Boundary Events. |
Response codes
- 200 — task failed (or was already terminal — idempotent).
- 400 —
{"error": "ErrorMessage is required"}. - 404 —
{"error": "User task '<id>' not found"}— task never existed.
Curl example
curl -k -X POST https://localhost:7140/UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/fail \ -H "Content-Type: application/json" \ -d '{"errorMessage":"User rejected the task","errorCode":"400"}'POST /UserTasks/{activityInstanceId}/cancel
Section titled “POST /UserTasks/{activityInstanceId}/cancel”Cancels a pending user task. The activity is marked terminal with ActivityCancelled and removed from the task list. No error propagation occurs — the workflow branch simply stops at the cancelled task. The request body is optional.
Request body (optional)
{ "reason": "Operator cancelled" }| Field | Type | Required | Description |
|---|---|---|---|
reason | string? | No | Free-text cancellation reason stored in logs. |
Response codes
- 200 — task cancelled (or was already terminal — idempotent).
- 404 —
{"error": "User task '<id>' not found"}— task never existed.
Curl example
curl -k -X POST https://localhost:7140/UserTasks/8b2e1a7c-9d3f-4e5b-a1c2-d3e4f5a6b7c8/cancel \ -H "Content-Type: application/json" \ -d '{"reason":"Operator cancelled"}'See also
Section titled “See also”- User Tasks guide — conceptual model, state diagram, BPMN authoring (
<fleans:expectedOutputs>). - Authentication — opt-in JWT bearer auth that gates every API endpoint, including the User Task surface.
Instance State endpoint
Section titled “Instance State endpoint”GET /Instances/{instanceId}/state returns a per-instance state snapshot including activeActivityIds, completedActivityIds, isStarted, isCompleted, isCancelled, isFailed, businessId, stateVersion, isFromReadModel, and related fields.
stateVersion is the event-log version the snapshot reflects, and isFromReadModel says where it came from: false — the instance’s grain, its exact current state; true — the read model, as of that version.
isCompleted means terminal. Tell the outcomes apart with the two flags: isCancelled: true → cancelled, isFailed: true → an unhandled activity failure ended the instance (see Unhandled errors fail the instance); both false → completed successfully. The failing activity is listed in completedActivities with a non-null errorState.
This endpoint is intended for diagnostics and load-test polling, not for high-frequency production use. A running instance is answered by its grain (live state; an idle instance is activated for it). A finished one is answered from the read-side EF projection, which the projector writes shortly after it completes; until then the grain answers, so the response always reflects your own last call. Instance lists (GET /Definitions/{key}/instances) read only the projection and can lag a just-started or just-finished instance by a moment; each row’s stateVersion is the event-log version it reflects.
curl -k https://localhost:7140/Instances/<guid>/state
-k(or--insecure) skips dev-cert validation. In production behind a proper TLS cert, drop the flag.
Returns 404 with {"error":"Instance {id} not found"} if no instance with that ID was ever started.
Rate limiting: uses the polling policy. See Rate Limiting below for opt-in semantics.
Purge endpoint
Section titled “Purge endpoint”POST /Instances/purge deletes everything stored for workflow instances that finished (completed, cancelled or failed) before CompletedBefore: their event journal, snapshot, read-model rows and user tasks. Running instances are never touched. A child instance is kept while its parent is still running. Fleans does not run this on a schedule; call it from your own scheduled job. When and why to run it: Retention.
| Field | Required | Default | Notes |
|---|---|---|---|
CompletedBefore | yes | — | ISO-8601 timestamp. Must be at least 1 hour in the past. |
BatchSize | no | 500 | Batch size for each category, 1–5000. |
MaxInstances | no | 10000 | Maximum instances and, separately, message keys examined per call. |
curl -k -X POST https://localhost:7140/Instances/purge \ -H 'Content-Type: application/json' \ -d '{"CompletedBefore":"2026-07-01T00:00:00Z"}'# {"purgedCount":10000,"hasMore":true,"purgedMessageDeduplicationsCount":500}Each call deletes at most MaxInstances instances, oldest first. Call it again until hasMore is false. Returns 400 when CompletedBefore is missing or less than an hour ago, or when a size is out of range.
It also examines up to MaxInstances message-idempotency records in batches of BatchSize and
deletes those whose 24-hour window has expired, independently of CompletedBefore. This runs
even when there are no instances to delete. purgedCount counts instances only;
purgedMessageDeduplicationsCount counts deleted message records. hasMore covers both categories.
Purged data cannot be restored. Back up first if you need to keep it.
Rate limiting: uses the admin policy.
Rate limiting
Section titled “Rate limiting”All API endpoints have rate-limiting attributes (workflow-mutation, task-operation, read, admin, polling), but rate limiting is opt-in: it only activates when the RateLimiting configuration section is present. Default appsettings.json has no such section, so the middleware is off by default. When activating rate limiting, populate all five policies together — partially populating the section causes unregistered-policy endpoints to return HTTP 500.
Configuration example
Section titled “Configuration example”Add this to your appsettings.json (or appsettings.Production.json):
{ "RateLimiting": { "WorkflowMutation": { "Window": 60, "PermitLimit": 100 }, "TaskOperation": { "Window": 60, "PermitLimit": 100 }, "Read": { "Window": 60, "PermitLimit": 200 }, "Admin": { "Window": 60, "PermitLimit": 50 }, "Polling": { "Window": 1, "PermitLimit": 1000 } }}Window— time window in seconds (default: 60)PermitLimit— maximum requests per window per client IP (default: 100)
The rate limiter uses a fixed window algorithm, partitioned by the client’s RemoteIpAddress.
Policy → endpoint mapping
Section titled “Policy → endpoint mapping”Paths are relative to the API base URL.
| Policy | Endpoints | Description |
|---|---|---|
WorkflowMutation | POST /Execution/start, /Execution/message, /Execution/signal, /Execution/evaluate-conditions, /Definitions/deploy, /Deployments/requests | Workflow lifecycle write operations (start instance, deliver event, evaluate conditions, deploy BPMN) |
TaskOperation | POST /Execution/complete-activity, /UserTasks/{activityInstanceId}/claim, /UserTasks/{activityInstanceId}/unclaim, /UserTasks/{activityInstanceId}/complete, /UserTasks/{activityInstanceId}/fail, /UserTasks/{activityInstanceId}/cancel | Activity-completion + user-task operations — see User Tasks guide |
Read | GET /Definitions, /Definitions/{key}/instances, /Definitions/{key}/{version}/instances, /UserTasks, /UserTasks/{activityInstanceId}, /Deployments/requests, /Deployments/requests/{id}, /Deployments/requests/{id}/bpmn, /Definitions/latest | Read-only queries |
Admin | POST /Definitions/disable, /Definitions/enable, /Instances/purge, /Deployments/requests/{id}/approve, …/reject, …/withdraw, …/retry | Admin operations on process definitions, deployment reviews and instance retention |
Polling | GET /Instances/{instanceId}/state | High-frequency state polling |
Environment variable overrides
Section titled “Environment variable overrides”For Docker Compose or container deployments, use __ (double underscore) notation:
RateLimiting__WorkflowMutation__PermitLimit=1000RateLimiting__WorkflowMutation__Window=1RateLimiting__Polling__PermitLimit=10000Important: Either configure all five policies or leave the
RateLimitingsection absent entirely. A partial configuration will cause HTTP 500 errors on endpoints whose policy is not registered.
Adaptive admission limit
Section titled “Adaptive admission limit”Independently of the fixed policies above, the WorkflowMutation and TaskOperation endpoints sit behind an adaptive concurrency limit that is on by default (Fleans:Admission). It shrinks when the p95 latency of those requests passes a target (500 ms by default) and grows back slowly once the cluster keeps up, so an overloaded cluster answers 429 Too Many Requests with Retry-After: 1 instead of timing out:
{ "error": "The cluster is at its admission limit. Retry later." }Clients should retry a 429 after Retry-After seconds, with jitter. Keys and defaults: Configuration reference.
Authentication
Section titled “Authentication”The API supports opt-in JWT bearer authentication, disabled by default. See Authentication for configuration, provider walkthroughs, and client examples.