---
title: Lifecycle Status
slug: lifecycle-status
kind: pattern
summary: One named state per record, drawn from a small closed set that everyone in the business already says out loud — the single field that answers "what happens to this next".
problem: >-
  A record's real state is spread across five booleans and two timestamps, so
  nobody can answer "where is this?" without reading all of them and knowing the
  precedence. Two people read the same row and reach different conclusions.
family: [orient, workflow]
data_shape: [record]
principles: [orientation, consistency]
interaction: [scanning]
density: low
complexity: low
status: stable
visibility: public
use_when:
  - The record moves through stages and different stages mean different work.
  - People ask "what stage is X at?" in conversation.
  - You are about to add a third boolean that interacts with the first two.
avoid_when:
  - The record genuinely has independent flags — archived and starred are not stages of one lifecycle, they are two different facts.
  - There is exactly one state. Then it is not a lifecycle, it is a fact.
alternatives:
  - slug: object-identity-header
    when: You have the state and need somewhere prominent to put it.
  - slug: linear-workflow
    when: The states are settled and you now need to move records between them.
ask_leo: |
  Replace the boolean flags on this model with one lifecycle status field.

  - Use a small closed set of named states, in the words the business already
    uses. Six or fewer is usual; more than about eight means two lifecycles are
    tangled together.
  - Store the state as a single value, not as several booleans that have to be
    read in a precedence order.
  - Every state must have a plain-English name a non-technical person would use
    in a sentence.
  - Render the state as a word everywhere it appears. Colour is optional and
    must never be the only carrier of the meaning.
  - Reserve the attention colour for the states that need a human to act. States
    that are simply where the record sits are neutral grey.
  - Keep flags that are genuinely independent — archived, flagged, starred — as
    separate fields. Do not fold them into the lifecycle.
related:
  - title: Color, Icons, and Contrast
    url: /cookbook/color-icons-and-contrast
    summary: How to render the state so colour never carries the meaning alone.
  - title: One consistent pattern
    url: /patterns/consistency
    summary: One object, one vocabulary, everywhere it appears.
---

## 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?` plus `reviewed?` plus
  `scheduled_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](/patterns/dense-operational-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.
