Reference · Work orders

Work order state machine

Every work order in Knurl has a state, and every state change is an event recorded in the append-only event log. The log is the source of truth — the current state is just a cache of the latest event. This page documents every state and every event so you know exactly what each button does.

States

A work order can be in one of these 10 states:

  • reported — A technician or guest scanned a QR and reported an issue. Awaiting manager triage.
  • draft— A manager started writing a work order but hasn't submitted it yet. Not visible to technicians.
  • open — Triaged and ready to assign.
  • assigned — A technician has been assigned. Waiting for them to start work.
  • in_progress — The assignee has started work.
  • on_hold — Work paused. Reason recorded.
  • pending_review — Technician marked complete and attached photo proof. Awaiting manager approval.
  • completed — Manager approved. The fix is done.
  • closed — Final. Archived for reporting.
  • cancelled — Terminated before completion.

Events

13 events move work orders between states.

Triage

  • created → reported | draft | open. Technicians can only create asreported; managers can create at any of the three start states.
  • triaged: reported → open. Manager confirms it's real work.
  • rejected_report: reported → cancelled. Manager dismisses (duplicate, not an issue, etc.).
  • submitted: draft → open. Manager finishes drafting and submits.

Assignment

  • assigned: open → assigned. Requires a new_assignee_id.
  • reassigned: assigned | in_progress → assigned. Same.

Execution (assignee only)

These transitions require the actor to bethe assignee. A manager who isn't the assignee cannot start, hold, resume, or complete the work — they reassign to themselves first if they want to do it.

  • started: assigned → in_progress.
  • held: in_progress → on_hold. Requires a reason (free text).
  • resumed: on_hold → in_progress.
  • completed: in_progress → pending_review. Requires at least one photo and notes. Without those, the API rejects the event.

Review & close

  • approved: pending_review → completed. Manager accepts the work.
  • rejected: pending_review → in_progress. Manager kicks it back with a comment.
  • closed: completed → closed. Archives the work order for reporting. Often automated.

Cancellation

  • cancelled: any non-terminal state → cancelled. Terminal. Used when the underlying work is no longer needed.

Why a transition can fail

When the API or mobile app rejects an event, it returns a machine reason and an HTTP status:

  • illegal_transition→ 409. You tried to move to a state that isn't reachable from where you are. (E.g. trying to starta work order that's already in progress.)
  • not_assignee→ 403. You tried to fire an assignee-only event but you're not the assignee.
  • forbidden→ 403. Your role doesn't permit this action. (E.g. a technician trying to triage.)
  • asset_not_found → 404. The asset this work order references no longer exists.
  • not_found→ 404. The work order itself doesn't exist or is in a different facility.
  • photo_required → 400. completed fired without any photo IDs.
  • notes_required → 400. completed fired without notes.
  • reason_required → 400. held or rejected fired without a reason.

What technicians see

The mobile app mirrors the same state machine. If a technician tries to complete a work order without photos while offline, the completion is queued but rejected at sync time — and the rejected-op card surfaces the reason so they can attach photos and try again. Nothing is lost.

More in the field guide →

Why append-only

Every state change is an event row in work_order_events, timestamped, attributed to the actor. The currentstatus on the work order is a projection of the latest event. This means:

  • The full lifecycle is a permanent audit trail — never edited, never deleted.
  • Reports project over events, not over a snapshot — so MTTR and throughput are always consistent with what actually happened.
  • Offline conflict resolution is deterministic — events have server-side ordering, so two technicians' edits resolve predictably.
Last reviewed 2026-05-17 · Owner: product · Verified against app release 2026.05.17 (commit 9e39bf7)