Tasks

The unit of work the server delivers to a worker — lifecycle, version-based concurrency, and lease semantics.

Unit of delivery

A task is the unit of work the server delivers to a worker. Where a Promise represents a future value, a task represents the responsibility for producing that value — claimed by a worker, kept alive by a lease, and surrendered on crash so a successor can take over.

A task and its associated promise share an identifier. They are paired but distinct: the promise owns the value; the task owns the claim. Tasks exist only when work needs to be delivered to a worker — that is, when the promise carries a resonate:target tag specifying a delivery address.

Lifecycle#

A task moves through five states. Transitions back to pending increment the task's version; the version is the optimistic concurrency control (OCC) token every mutating operation must present.

code
          create (via promise.create with resonate:target)
            |
            v
        [pending] <---(release)--- [acquired] <--- create (via task.create)
            ^                         |   ^
            |                      (suspend)
            |                         |   |
            |                         v   |
            |                     [suspended]
            |                         |
            +---(resume, version++)---+

        [acquired] ---(fulfill)---> [fulfilled]
StateMeaning
pendingThe task is available to be claimed. The server has enqueued an execute message to the resonate:target address.
acquiredA worker has claimed the task and is making progress. The worker holds a lease that must be refreshed via heartbeat.
suspendedThe worker has declared it is awaiting one or more other promises. The server holds the task here until any awaited promise settles.
fulfilledTerminal. The associated promise has settled and the task is complete.
haltedTerminal admin state for tasks that should not progress. (See operations below.)

task.create creates the task directly in acquired state. promise.create with a resonate:target tag creates the task in pending state. The version field increments on every transition back to pending (release, resume, lease expiry); it does not change on pending → acquired.

All mutating task operations require the caller to present the current version. A version mismatch results in conflict (status 409) — the task has moved on without you, and the operation must be re-fetched and retried.

Lease and heartbeat#

An acquired task carries a lease: a deadline by which the worker must signal liveness. The lease is set when the task is acquired and refreshed by every successful heartbeat. If the lease expires, the server releases the task back to pending with an incremented version, allowing a different worker to claim it.

code
acquire     →  set lease = now + leaseTimeout
heartbeat   →  set lease = now + leaseTimeout
release     →  delete lease, version++
lease expiry → release back to pending, version++

Heartbeat is process-level, not task-level. A worker sends one heartbeat for all the acquired tasks it holds; the server refreshes every matching (id, version) pair in a single round-trip.

The heartbeat cadence is implementation-defined. A typical worker heartbeats at roughly half the lease interval to absorb transient network delay without losing the lease.

Operations#

The server exposes the following task operations. Each operation takes the task's current id and version (where applicable) and returns the new state or an error.

OperationPurpose
task.getRetrieve a task by id (read-only).
task.createCreate a task and its promise atomically; the task starts in acquired. The action must carry a resonate:target tag.
task.acquireTransition pending → acquired. Only the worker presenting the current version succeeds; concurrent workers see 409.
task.heartbeatRefresh the lease on one or more acquired tasks. Each (id, version) pair is processed independently; mismatches are silently skipped.
task.suspendDeclare the worker is awaiting one or more promises. The server registers callbacks atomically and transitions the task to suspended. If any awaited promise has already settled, the server returns 300 — the worker should resume immediately without suspending.
task.fulfillSettle the task's promise and transition the task to fulfilled in one atomic operation. The action's promise id must equal the task id.
task.fenceConditional fulfill: settle only if the task is still acquired at the presented version. Used when the worker needs to know it is the unique current claimant before externalizing a side effect.
task.releaseSurrender the claim. The task transitions back to pending with an incremented version, becoming available to other workers.
task.haltAdministratively block the task from being claimed or resumed.
task.continueReverse task.halt.
task.searchEnumerate tasks (filtered by state, target, etc.).

Asymmetry with promises#

Task operations are not idempotent in the way promise operations are. A promise carries idempotency keys (ikc, iku) that allow safe retry after an unknown-outcome failure (see the Durable Promise Specification). A task carries only its version — retrying a successful task.acquire with the same version succeeds again at first but fails as soon as the next operation increments the version.

This asymmetry reflects the role of each: a promise represents state whose final value is what matters; a task represents progress, where each transition is a decisive moment of claim ownership. The version protects against duplicate progress; idempotency keys protect against duplicate state.

Invariants#

A conformant server must maintain the following invariants over its task store:

  • orphan_tasks — every task must have a corresponding promise.
  • pending_task_no_ttimeout — every pending task must have a retry timeout.
  • acquired_task_no_lease — every acquired task must have a lease.
  • suspended_no_callback — every suspended task must have at least one callback registered on an awaited promise.
  • suspended_with_consumed_callbacks — no suspended task should have any consumed callbacks.
  • suspended_task_has_ttimeout — no suspended task should have any timeout.
  • fulfilled_task_has_ttimeout — no fulfilled task should have any timeout.

Violations indicate a bug in the implementation, not a recoverable runtime condition.

See also#