Worker Protocol

This is the language-agnostic wire contract that every reference SDK in workers/ conforms to. Read this before writing a new SDK; if the wire shape changes, this document changes first and every SDK follows.

The contract is the engine’s external-task API — workers are clients of /api/v1/external-tasks/*. The engine doesn’t know about workers as a concept beyond “polls for work and reports back.”

Base URL and authentication

  • Base URL: configurable, e.g. http://localhost:8080. All paths below are relative to this.
  • Auth: optional Authorization: Bearer <token> header. The reference engine accepts unauthenticated requests in dev; production deployments enforce a token. SDKs MUST treat the token as a sensitive value.
  • Content type: application/json request and response, UTF-8.

Endpoints

1. Fetch and lock — POST /api/v1/external-tasks/fetch-and-lock

Long-poll for tasks. The engine returns 0..N tasks whose locked_until is null or has expired, locks them to worker_id for lock_duration_secs, and ships their current process variables.

Request

{
  "worker_id": "http-worker-7d2",
  "topic": "http.call",
  "max_jobs": 10,
  "lock_duration_secs": 30
}
FieldRequiredNotes
worker_idyesStable identifier for this worker process. Reusing the ID across restarts is fine.
topicoptionalFilter by <conduit:taskTopic>. Omit to receive any topic (rarely useful).
max_jobsoptional, default 10, capped at 100Upper bound on tasks returned this round.
lock_duration_secsoptional, default 30TTL on the lock. Worker must complete / fail / extend within this window.

Response — 200 OK

[
  {
    "id": "0a3...",
    "topic": "http.call",
    "instance_id": "...",
    "execution_id": "...",
    "locked_until": "2026-05-08T10:00:30Z",
    "retries": 3,
    "retry_count": 0,
    "variables": [
      { "name": "order_id", "value_type": "String", "value": "ord-42" },
      { "name": "amount",   "value_type": "Long",   "value": 1500 }
    ]
  }
]

variables is the full set of process-instance variables visible to this task (same scope rules as a serviceTask in the engine). If no tasks are available, the engine may either return [] immediately or hold the request open briefly (Phase 17 long polling).

2. Complete — POST /api/v1/external-tasks/{id}/complete

Report the task as completed successfully. Optionally write process variables back to the instance.

Request

{
  "worker_id": "http-worker-7d2",
  "variables": [
    { "name": "http_status", "value_type": "Long", "value": 201 },
    { "name": "order",       "value_type": "Json", "value": { "id": 42 } }
  ]
}

worker_id must match the worker that holds the lock; the engine rejects the call otherwise. variables is optional — omit or send [] to complete with no variable changes.

Response — 204 No Content.

3. Failure — POST /api/v1/external-tasks/{id}/failure

Report a transient/system failure. The engine decrements retries and re-locks the task for another worker after the lock TTL. When retries reaches 0, the task transitions to failed and stops being re-delivered.

Request

{
  "worker_id": "http-worker-7d2",
  "error_message": "Connection refused: api.example.com:443"
}

Response — 204 No Content.

Use this for failures the BPMN does not need to know about — connection errors, timeouts, transient 5xx upstream responses.

4. BPMN error — POST /api/v1/external-tasks/{id}/bpmn-error

Throw a BPMN error. This routes the process down a boundaryErrorEvent if one matches error_code, or terminates the instance with an error if not.

Request

{
  "worker_id": "http-worker-7d2",
  "error_code": "PAYMENT_DECLINED",
  "error_message": "card_declined",
  "variables": [
    { "name": "decline_reason", "value_type": "String", "value": "insufficient_funds" }
  ]
}

Response — 204 No Content.

Concernfailurebpmn-error
Decrements retries?YesNo
Re-delivered?Yes (until retries=0)No
Branches the BPMN?NoYes (if a matching boundary event exists)

5. Extend lock — POST /api/v1/external-tasks/{id}/extend-lock

Refresh the lock without completing. Use this for handlers that legitimately need longer than lock_duration_secs.

Request

{ "worker_id": "http-worker-7d2", "lock_duration_secs": 60 }

Response — 204 No Content.

Variable shape

Every variable on the wire is { name, value_type, value }. value_type is one of:

value_typeJSON value shape
Stringstring
Longinteger (i64)
Doublenumber (f64)
Booleanboolean
Jsonany JSON value (object, array, etc.)
Nullnull

SDKs SHOULD provide constructors per type (Variable.string("name", "value"), etc.) so users don’t have to remember the strings.

Error responses

Non-2xx responses from the engine carry the structured-error envelope:

{ "code": "U001", "message": "...", "action": "..." }

U-prefix codes are user/client errors (4xx). S-prefix codes are system errors (5xx) and never leak internals. SDKs SHOULD surface code and message in their thrown error so users have an actionable handle.

Semantics workers must understand

At-least-once delivery

A worker that crashes between starting the side effect and calling complete will see the task re-delivered once the lock TTL expires. Side effects must be idempotent under retry. See Idempotency.

worker_id is a lock owner, not an identity

Two workers with the same worker_id will fight over locks. Run with distinct IDs in production (hostname + PID + UUID is fine).

Lock TTL is a soft contract

If a worker exceeds lock_duration_secs without calling extend-lock, the engine considers the lock expired and may hand the task to another worker. The original worker’s eventual complete call is rejected — SDKs SHOULD log this distinctly so users can spot under-sized lock TTLs.

Order is not preserved

fetch-and-lock returns tasks in an unspecified order. Workers MUST NOT assume in-order delivery within or across instances.

Variables are a snapshot

Variables shipped with a task reflect the instance state at lock time. The complete call’s variables field is the worker’s full reply; the engine merges it into the instance.

SDK conformance checklist

A new SDK in workers/<lang>/ SHOULD provide:

  • Client wrapping the 5 endpoints above
  • Configurable base URL, optional bearer token, request timeout
  • Handler interface / trait / abstract class with topic() + handle(task) → Complete | BpmnError | failure
  • Runner that loops fetch-and-lock → handle → report, with configurable poll interval and lock duration
  • Idiomatic registration sugar — proc-macro / annotation / decorator / builder / register-by-name
  • At least one integration test against a mock engine
  • README documenting quick-start and idempotency expectations