Skip to content

Why Every Workflow Is Really a State Machine

A status dropdown is not a workflow. Mental Models #14 on illegal transitions, named commands, and why every real workflow is a state machine.

Mental Models #14 · Part of Binary and Beyond. LinkedIn newsletter edition follows.

The product has a status field.

Packed. Shipped. Delivered. Returned.

The UI offers a dropdown. Support can pick any value. Ops can pick any value. A nightly job can pick any value. The ticket said "workflow." What shipped was a column and a form.

On a busy Thursday, someone marks an order shipped from packed. Quality never ran. The carrier label never printed. The customer gets a tracking number for a box that is still on a bench.

The button worked.

The workflow did not.

That is why every real workflow is a state machine, whether you drew the circles or not. A workflow is the set of legal transitions, who may fire them, and what must already be true. A status dropdown is a way to skip that design and still look finished in the demo.

The skip that looked like progress

Picture a mid-market fulfilment desk built the usual way.

Orders live in a table. status is a string. The screen has Edit.

The intended path is obvious to the people who built it: paidpackedqcshippeddelivered.

Nothing in the software knows that path.

A warehouse lead, clearing a backlog, sets shipped from packed because "we always skip QC when the van is waiting." A support agent, looking at an angry ticket, sets delivered because the customer said it arrived. A retrying webhook sets paid again on an order that already left the building.

Each writer has a screenshot. Each screenshot is true inside their screen. The product now believes three histories.

Production Notes #07 argued that CRUD dies when the noun is also a promise. This is the next sentence of that argument. Once you admit the noun is a promise, you still need a model of what is allowed next. Without it, "update status" is how illegal futures get written as facts.

Postgres was fine. The missing machine was not a datastore. It was the list of transitions nobody wanted to name in the sprint.

A dropdown is not a product model

Status strings feel like workflow because humans read them as a story.

Software does not.

Software sees: a field that accepts a write. Preconditions are optional. Side effects are whoever subscribed. Ownership is whoever has the button.

A state machine is the opposite kind of object:

  • Named states with meaning that outlives the UI copy
  • Named transitions that are the only legal moves
  • Guards that refuse a move when a side effect has not happened, or has already happened
  • Actors bound to transitions, not to a global edit permission
  • Effects that fire because the transition fired, not because a column changed

You do not need a library with "FSM" in the name. You need those five things to be true in production. A case statement with an audit row can be a state machine. A kanban board with unrestricted drag-and-drop is not.

The test is simple. Can you name a transition that should 409, even when the user is admin?

If the answer is "we validate in the controller," you still have a dropdown with manners. You do not yet have a workflow.

What the brief asked forWhat production needed
A status fieldNamed states with forbidden skips
An edit buttonCommands: PassQc, Dispatch, MarkDelivered
Notifications on saveEffects tied to the transition, not the column
Admin overrideA logged exception transition, not a hole in the model

Where teams go wrong

They model the happy path as a list. Real orders rewind, split, wait on a third party, and get corrected. A list of steps is a ceremony. A machine has to say what happens when step 3 arrives before step 2.

They let every role share one write. Support, warehouse, finance, and the integration user are not one actor. If they share PATCH /orders/:id, they will invent each other's jobs. Why race conditions are business problems is the concurrent version of the same failure: two legal-looking writes, one broken promise.

They treat "shipped" as a label instead of a commitment. Shipped means a carrier exists, a label exists, inventory left a location, a customer was told something true. Setting the string without those facts is how you manufacture divergent state inside one application.

They skip the machine because "it is just an internal tool." Internal tools are where irreversible work actually happens. Customers see the lie later. Ops live in it now.

They add a second status field when the first one gets political. ops_status and customer_status without a transition map is two dropdowns. You have not added a workflow. You have added a translation problem.

What a machine looks like in boring software

Mature systems stop asking "what status should we store?" and start asking "what happened, and what is allowed next?"

That usually looks like:

Commands, not field writes. CapturePayment, PassQc, DispatchShipment, RecordDelivery, OpenReturn are different operations with different preconditions. A blanket update is how QC disappears on a Thursday.

Explicit illegal. Packed cannot go to delivered. Paid cannot go back to draft after money moved. Cancelled cannot ship. If you cannot list the illegal moves, the UI will discover them for you.

Idempotent transitions. The van driver taps complete twice. The webhook retries. Idempotency is how "twice" stays a non-event instead of a second shipment.

History that survives the label. Overwriting status erases the evidence you will need when finance, the carrier, and the customer disagree. Append what happened. Display the current state as a projection.

Override as a first-class transition. Real warehouses skip QC sometimes. Encode ForceDispatch with a reason, an actor, and an audit row. A hole in the dropdown is not operational flexibility. It is an unlogged exception.

We rebuilt exactly this class of problem on a live dealer portal: fitment orders that used to live in email threads, with several organisations sharing one order. The Port of Entry workflow only became safe when stages were role-gated transitions, not a linear form anyone could advance. The hard work was not another screen. It was making every hand-off refuse the wrong next step.

AI-shaped products make the skip cheaper

An agent that can "update the order" is a dropdown with confidence.

If your domain is a bag of fields, the model will invent transitions no operations lead approved, in language that sounds careful. If your domain is commands with guards, the agent has fewer ways to ship a box that was never packed.

The control layer is not the LLM. It is whether the system still thinks a workflow is a form.

Tool-calling without a machine is how you get a polite paragraph and a corrupted promise in the same response.

Questions to ask before the next status dropdown

I use this checklist when a brief says "add a workflow" or "just add statuses for X."

  1. What are the states, in words a war room would use? If two teams disagree on what done means, you do not have states yet. You have labels.
  2. Which transitions are illegal even for admin? Name five. If you cannot, the dropdown will invent them.
  3. Who is allowed to fire each transition? Support, warehouse, finance, and the integration user are not one writer.
  4. What must already be true? Label printed. Payment captured. QC passed. Inventory decremented. Write the guards.
  5. What becomes true after? Notifications, ledger rows, customer-visible copy. Tie effects to the transition.
  6. If this transition runs twice, what remains true? If you shrug, you are not ready for webhooks or a mobile tap with a flaky radio.
  7. How do exceptions enter the log? Skip, rewind, and force need names. Hidden edits are how you lose the argument with the customer.
  8. Are we drawing a happy path and calling it a workflow? Draw the skip, the retry, and the return. That is the product.

If the answers collapse to "the UI will only show valid options," you have hidden the machine in CSS. A second client, a job, or an agent will not read your CSS.

A different standard for "workflow done"

A workflow is not done when the dropdown has the right labels.

It is done when you can explain, for every irreversible commitment the business makes, which command creates it, which states forbid it, and who is allowed to run it after the first side effect has already fired.

That standard feels heavy for an internal screen.

It feels obvious the first time a box ships without QC and everyone can prove they only pressed a button the UI offered.

The teams I trust in production are not the ones with the prettiest kanban. They are the ones who noticed that a status field was already a state machine, then made the illegal moves refuse to compile in production.


A workflow is not a status field.

It is the set of futures you are willing to defend.

Related reading


Mental Models #14 · Part of Binary and Beyond. LinkedIn newsletter edition follows. Building enterprise platforms whose workflows refuse illegal skips? Start a conversation.

Agency partner

Need delivery stability without adding headcount?

Quick Brown Fox helps agencies ship complex web platforms, tighten QA, and scale engineering capacity—without becoming a liability to your client relationships.