statefultask aggregate

Field-service task lifecycle

Create, assign, accept, travel, prove, approve, and close a field job without losing actor, retry, or evidence history.

Every state means something operational

A state is not a UI label. It determines which actor may act, what evidence exists, what may be retried, and how recovery proceeds.

01
unassigned

Unassigned

The job exists with client, site, SLA, skills, window, and proof policy but no worker owns it.

02
assigned

Assigned

A specific eligible worker owns the next decision and dispatch has recorded why they were selected.

03
accepted

Accepted

The worker has acknowledged responsibility and the customer-facing plan can become firm.

04
en_route

En route

Travel has begun and ETA, route deviation, and SLA-risk observations may change continuously.

05
in_progress

In progress

Arrival is established and work evidence can be gathered under the declared proof policy.

06
proof_pending

Proof pending

The worker submitted an immutable evidence set awaiting automated or supervisor validation.

07
completedterminal

Completed

Required proof is accepted and downstream billing, inventory, SLA, and customer workflows may run.

08
cancelledterminal

Cancelled

A named actor stopped the job with a reason before completion.

Transitions are attributable and retry-safe

Commands express intent. The aggregate validates current state and invariants, commits one new version, and emits a fact in the same transaction.

Command & actorAllowed fromResultIdempotency strategy
create_taskIntegration service
new aggregate
unassignedtask.created

Use the upstream job ID as a stable creation key.

assignDispatcher
unassignedassigned
assignedtask.assigned

Deduplicate by dispatcher command ID and compare expected task version.

acceptField worker
assigned
acceptedtask.accepted

Generate the key on-device before network transmission and persist it through retry.

start_travelField worker
accepted
en_routetask.en_route

A repeated start must return the same state and must not create a second trip.

arriveField worker
en_route
in_progresstask.started

Bind arrival evidence and command ID to the same transition transaction.

submit_proofField worker
in_progress
proof_pendingtask.proof_submitted

Hash the evidence manifest and reject a reused key with different content.

approve_proofSupervisor
proof_pending
completedtask.completed

Commit approval and downstream outbox entries atomically.

reject_proofSupervisor
proof_pending
in_progresstask.proof_rejected

Preserve decision reason and the rejected evidence version.

Persist enough to recover and explain

Keep provider responses, business identity, state, events, and side-effect delivery distinct so each can be reconciled safely.

Task snapshot

Fast current-state reads and optimistic concurrency.

taskIdexternalIdstateversionassigneeIdmapplsPin

Audit event

Attributable, replayable history for support and compliance.

eventIdaggregateVersionactorcommandIdoccurredAt

Evidence manifest

Immutable references and hashes for checklist, media, signature, and consent.

manifestIdtaskVersioncaptureTimecontentHashretentionClass

Transactional outbox

Reliable downstream billing, inventory, notification, and analytics delivery.

outboxIdeventIdstatusattemptsnextAttemptAt

Protect truth at the aggregate boundary

  1. 1

    One upstream external job maps to one durable task aggregate.

  2. 2

    Only the assigned worker can accept, travel, arrive, or submit proof unless an attributable override is recorded.

  3. 3

    Every command carries tenant, actor, idempotency key, expected version, and occurrence time.

  4. 4

    Completion is impossible until the declared proof policy passes.

  5. 5

    Task history is append-only; corrections use new events or compensating work.

Know what to do before it breaks

Recovery changes durable truth only through the same rules as normal operation. A timeout is an unknown outcome, not evidence that nothing happened.

Create-task response times out

Detect

No provider response but the upstream external ID and idempotency key are known.

Recover

Query by known identity or repeat the same command; do not mint a new job ID.

Two dispatchers edit the same task

Detect

The submitted expected version is older than the current aggregate version.

Recover

Return conflict with current state; refresh context and require an intentional new command.

Worker is offline during proof capture

Detect

Evidence exists locally but no server acknowledgement or event ID exists.

Recover

Retain command ID, hashes, capture timestamps, and retry queue until the committed event is returned.

Downstream system is unavailable after completion

Detect

Task is complete but its outbox entry remains pending or retrying.

Recover

Retry outbox delivery independently; never reopen or re-complete the task to trigger side effects.

Observe the journey, not merely HTTP status

Measure state age, event health, retries, reconciliation, and sensitive-data access alongside latency and error rate.

  • Command acceptance, rejection code, actor, task version, and latency
  • Time spent in each state and SLA-risk interval
  • Duplicate command rate and version-conflict rate
  • Offline queue age and proof upload completeness
  • Outbox backlog, attempt count, and dead-letter age
  • Precise-location access with purpose and retention class

Contracts, repositories, workshops, tutorials, and runnable evidence

Only explicitly indexed evidence is linked. Empty sections are not backfilled with invented endpoints or package names.