Anatomy
Draft → In review → Approved → Scheduled → Complete
▲ │
└──────────── Changes requested ◀─────────┘
needs a human: In review, Changes requested → amber
just where it is: Draft, Approved, Scheduled → grey
finished: Complete → grey
Two rules do most of the work:
- One field, closed set. Not
approved?plusreviewed?plusscheduled_at. The moment state is derived from several fields, it has a precedence order, and precedence orders live in one developer's head. - The names are the business's, not the schema's. If people say "waiting on
the client", the state is not
pending_external. Vocabulary you have to translate is vocabulary people get wrong.
Why it works
It makes "what happens next" a lookup instead of an inference. That is worth more than it sounds: inference is where two colleagues quietly disagree, and the disagreement only surfaces when something has already gone wrong.
It also gives every other pattern something to hold on to. A dense table needs one column, not five booleans. A review queue needs one filter. A notification needs one trigger. The lifecycle field is the join between the data model and how people talk.
And it bounds colour. With a named set you can decide, once, which states need a human — and only those spend the attention colour. Without it, every flag argues for its own badge and you end up with badge soup.
Getting it wrong
- Boolean creep. Each flag was reasonable alone; together they encode a state machine nobody wrote down.
- Rainbow status. Six hues at equal saturation, some meaning two things. Under a squint test the table becomes confetti and colour stops separating anything.
- States that are really two lifecycles. "Draft, In review, Paid, Refunded" mixes an editorial lifecycle with a billing one. Split them.
- A state nobody can define. If two people describe "Pending" differently, it is not a state, it is a gap.
Exemplars
Stripe keeps payment status to a handful of words — succeeded, pending, failed, refunded, disputed — and the entire product is built on them. It is a good demonstration that a small closed set scales further than a flexible one.
GitHub pull requests have exactly three states (open, closed, merged) and push everything else — draft, review status, checks — into separate, clearly independent fields. That separation is the discipline most products skip.
The extractable rule: if you cannot name the states, you do not yet understand the process — and no amount of interface will cover that. Getting the vocabulary right is the design work; rendering it is the easy part.