ADR-0013: Event-Based API for Task Status Transitions
Status
AcceptedTags
fieldforce, state-machine, api, patch, events, transitionsDecision
Fieldforce task status changes are triggered by sending a named event (start, submit, approve, reject, resubmit, cancel) in the PATCH body — not a target status string. The server enforces all guards (role, current state, required fields) via the state machine before accepting any transition. The event key and field-update keys are mutually exclusive in the same PATCH body.
Why
A target-status API (PATCH { status: "completed" }) lets clients drive any transition by guessing the right status string. Guards in the state machine become unreachable because the handler must re-implement role and state checks before calling the machine — or skip them and let bugs through. Separate endpoints per transition proliferate routes without adding expressiveness.
Rejected alternatives:
- Target-status API (
PATCH { status: "in_progress" }): Business rules can be bypassed. Every consumer needs to know which status strings are valid from which state. Rejected. - Separate endpoints per transition (
POST /tasks/:id/approve): Multiplies routes. A single PATCH with an event discriminator is the idiomatic REST pattern for state machines. Rejected.
How it works
The PATCH body has two mutually exclusive shapes:| Event | From | To | Guard |
|---|---|---|---|
start | pending | in_progress | role ∈ |
submit | in_progress | completed | role = field_team |
approve | completed | approved | role ∈ |
reject | completed | needs_revision | role ∈ + review_note present |
resubmit | needs_revision | completed | role = field_team |
cancel | any non-terminal | cancelled | role ∈ |
TaskService.TransitionTask for event bodies and TaskService.UpdateTask for field-update bodies.
Rules for agents
- PATCH handler MUST reject with 400 if both
eventand any field-update key are present in the same body - Never accept a
statusfield in the PATCH body to change task state — useeventonly review_noteis required whenevent = "reject"— validate at handler level before calling the state machine- Do not add role checks in the HTTP handler that duplicate guard logic already in the state machine transitions