ADR-0013: Event-Based API for Task Status Transitions

Status

Accepted

Tags

fieldforce, state-machine, api, patch, events, transitions

Decision

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:
Valid events: Handler dispatches to TaskService.TransitionTask for event bodies and TaskService.UpdateTask for field-update bodies.

Rules for agents

  • PATCH handler MUST reject with 400 if both event and any field-update key are present in the same body
  • Never accept a status field in the PATCH body to change task state — use event only
  • review_note is required when event = "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

Bad pattern (do not generate)

Good pattern