Lifecycles & picklist options
Most things a workspace tracks move through states and then stop: a deal is prospecting, then qualified, then won or lost; a ticket is open, then resolved or cancelled; a job is scheduled, then done.
You model that with an ordinary picklist field. There is no separate “pipeline” object to set up — a lifecycle is a picklist whose options are listed in the order the process runs, with its end states marked. One table can have several independent lifecycle fields (a delivery phase and a billing status) because each is just a field.
Order is the order you type
Section titled “Order is the order you type”The order of the options is the order they are shown, everywhere: pickers, reports, and the columns of any board built on the field. There is no separate “sort” setting to keep in step.
So list a lifecycle in the order it actually runs, not alphabetically:
| Do | Don’t |
|---|---|
| Prospecting → Qualified → Proposal → Closed Won → Closed Lost | Closed Lost, Closed Won, Prospecting, Proposal, Qualified |
Use the ↑/↓ buttons beside each option in the field editor to move one. This is always a safe edit: the set of allowed values does not change, so no existing record is re-checked and nothing can be rejected.
Mark the end states
Section titled “Mark the end states”Each option has a lifecycle setting with three choices:
- Open — still in flight. This is the default; most options are open.
- Ended — success — the good outcome (Closed Won, Completed, Hired).
- Ended — failure — the other outcome (Closed Lost, Cancelled, Rejected).
Several options can share a setting. “Cancelled” and “Rejected” are both Ended — failure, and anything that asks “is this finished?” treats them the same.
Marking end states is what lets “still open” filters, funnel charts and win-rate reporting work without anyone hard-coding which values mean finished. Rename “Closed Won” to “Booked” and the reports keep working, because they follow the setting, not the name.
One thing an end state is not: frozen. Marking an option as ended is reporting and display information only. Reopening a closed ticket by moving it back to an open value is normal, and nothing blocks it.
Retiring an option you no longer use
Section titled “Retiring an option you no longer use”Sooner or later a stage stops being part of the process — but records that went through it are still there, and their history should stay truthful.
Untick Active on the option. That retires it:
- records already holding the value keep it, display it, and can still be edited and saved;
- nothing new can be set to it — not through the app, not through the API, and not through a CSV import;
- it disappears from the dropdown for everyone else.
Retiring is always allowed, even when thousands of records hold the value. It changes only the field’s settings, so it is instant.
If you edit a record that is parked on a retired value, the value stays in the dropdown for that record (marked (retired)) so that saving the record does not silently change it. Move it to a current value when you’re ready, and it disappears from that record’s list too.
Retire, don’t delete
Section titled “Retire, don’t delete”Deleting an option is the destructive alternative, and the system will refuse it while any record still holds the value. That refusal is deliberate: deleting would leave those records holding a value the field no longer defines.
Delete only options that were never used. Retire everything else.
Re-activating
Section titled “Re-activating”Tick Active again and the option is immediately selectable. Nothing was lost while it was retired.
Rules the field editor enforces
Section titled “Rules the field editor enforces”When you save a picklist, these are checked:
- every option needs a non-empty value (the stable machine name — the label is what people see, and may be changed freely);
- values must be unique within the field;
- at most one option can be the default, and the default cannot be a retired option.
The value is what is stored in the record and what the API and reports use, so
prefer short, stable ones (closed_won, not Closed Won — final!). Labels can
be edited any time without touching stored data; values cannot.
Designating the lifecycle field
Section titled “Designating the lifecycle field”Marking the end states describes the values. Designating the field says “this picklist is the lifecycle of this table” — which is what board and funnel views key off, and what lets you add movement rules.
A table can have several picklists and only some of them are lifecycles
(stage is; priority is not), so it has to be said explicitly. Designating a
field is a settings change: no data moves, nothing is rebuilt, and it can be
undone at any time.
Designating on its own restricts nothing. Every write that worked before still works. Rules are opt-in, one at a time.
Where to do it
Section titled “Where to do it”Open the table’s Schema page. Every picklist field has a stage button in its Actions column; a field that is already a lifecycle also carries a Stage chip next to its type, so you can see at a glance which fields on a table are pipelines. The button opens one dialog holding the whole machine:
- whether moves are unrestricted or limited to a matrix you tick,
- which values a record may start at,
- whether the value may be emptied once set,
- what must be filled in before a record may sit in each stage.
Saving an undesignated field designates it. Stop using as a stage removes the rules and leaves the field, its options and its data exactly as they were.
Two things the dialog does that are worth knowing about:
- It tells you what the rule implies before you save it — a stage nothing leads to, an end state that can still move on — and again afterwards if the server has anything to add. None of it blocks the save; each case is occasionally what you meant.
- Retired options keep their row in the matrix, marked Retired. An edge pointing at a retired value is inert rather than absent, and hiding the row would delete the rule the next time you saved.
Changing the rules needs permission to manage fields. Without it the dialog still opens read-only, because the rules are worth reading to anyone who has just had a write refused by them.
Starting stages
Section titled “Starting stages”initialStages says where a record may begin. It applies the first time
the value is set — whether that is when the record is created or later — and
never afterwards.
“Every deal starts at Prospecting” →
initialStages: ["prospecting"]
The transition matrix
Section titled “The transition matrix”Turn the policy to matrix and you list, for each value, the values it may move to. Everything you do not list is refused.
prospecting → qualified, closed_lostqualified → proposal, closed_lostproposal → closed_won, closed_lostclosed_lost → prospectingThree things about that example are worth pointing out, because they are the three things people get wrong:
- It is default-deny. Moving straight from Prospecting to Closed Won is refused, because that edge is not listed. That is the point of a matrix — but it means you have to list everything you want to permit.
- Closed Won has no line, so it is frozen. No record can leave it. If you want deals to be reopenable, give it an outgoing edge. A value’s end-state category does not freeze it — only the matrix does.
- Closed Lost has a way back. Backwards edges are easy to forget, and forgetting one traps records where they are. Ask “how does someone undo this?” for every stage before you save.
Two safety valves are always on, whatever the matrix says:
- Saving a record without changing its stage is never blocked. A frozen end state does not make its records read-only — you can still edit the amount on a won deal.
- Emptying the stage is governed by its own setting (
allowClear), not by the matrix.
When a move is refused, the error names the stage the record is in and lists exactly which stages it can move to.
Required fields for a stage
Section titled “Required fields for a stage”“A deal can’t be marked won without an amount” is a per-stage requirement: name the fields that must be filled in before a record may sit in a given stage.
This is checked against the record as it will be after the save — not against what you happened to edit. So:
- Moving a deal to Closed Won is refused when the deal has no amount, even if you only touched the stage. The message names the fields that are missing.
- Supplying the amount in the same edit as the stage change works.
- Emptying a required field while the record sits in that stage is refused too — otherwise the rule would only apply on the way in.
- An edit that touches neither the stage nor a required field is unaffected.
That last point is what stops a new requirement freezing your existing data: records already sitting in the stage without the value keep working, and are asked to comply the next time someone saves their stage or one of the required fields. (A full update through the API names every field, so it always counts as touching the stage — it is asserting the record’s entire state.)
Two things it is not:
- Not a substitute for making a field required. If a value is needed on every record whatever its stage, mark the field required. A per-stage requirement is for “needed by the time it reaches this point”.
- Not available on computed fields. You can only require fields a person can fill in; requiring a formula or rollup would give the user nothing to act on.
While you are editing a record, fields its chosen stage requires but that are still empty are flagged in the form, so you find out before saving rather than after.
A field named by a stage requirement cannot be deleted while the rule refers to it — update the stage configuration first. Otherwise the requirement would quietly stop applying, which is worse than it blocking.
Loading historical data
Section titled “Loading historical data”Records imported from an old system have been through stages in orders your new matrix would refuse. The recipe is import first, designate afterwards:
- Create the field and its options.
- Import the historical records, stages and all.
- Then designate the field and add the matrix.
The machine governs writes from the moment it exists, so anything already in the table is left exactly as it is. There is no override flag, and deliberately so: a rule with a bypass is a rule nobody can rely on.
When the stage is worked out rather than chosen
Section titled “When the stage is worked out rather than chosen”Some states are not a decision anyone makes — they follow from the data. A job with a completion date is finished. You can express that as a formula field and designate that as the stage, instead of asking people to keep a dropdown in step with the dates they already fill in.
The formula must return text, and you declare the values it produces:
| Value | Label | Category |
|---|---|---|
in_progress |
In progress | open |
completed |
Completed | terminal_success |
That list is what boards and reports use — the same ordered values with the same categories a picklist gives them, so everything downstream behaves identically. Time in stage, cycle time and the stage history panel all work.
Two things are deliberately different:
- A computed stage enforces nothing. No transition matrix, no starting stages, no per-stage required fields. The write that moves the stage is a write to some other field, so refusing it would mean telling someone they cannot save a completion date because of a rule about a field they never touched. If a rule has to be enforced, use a stored picklist.
- You cannot set it directly, in the app or through the API. To move the record, change the fields the formula reads. On a board, its columns show what is where but do not accept a drag — opening the record is how you move it.
The declared list is a description, not a constraint. Nothing stops a formula returning something you did not list — an extra branch added later, a typo in a string. Rather than hide those records, they collect in an Unknown column at the end. If you see one appear, the expression has a branch the list does not mention.
Choose a computed stage when the state genuinely follows from the data: it can never disagree with the record. Choose a picklist when a person decides, or when the process needs enforcing.
A worked example
Section titled “A worked example”The Sample Sales CRM workspace ships with all of this already set up on its
deals.stage field, so you can see the shape of a finished lifecycle rather
than build one from a blank page:
prospecting → qualification, closed_lostqualification → proposal, closed_lostproposal → negotiation, closed_lostnegotiation → closed_won, closed_lostclosed_lost → prospecting, qualificationwith Closed Won marked as a success end state, Closed Lost as a failure
one, new deals starting at Prospecting, and Closed Won requiring an amount and
a close date.
Four decisions in there are worth copying:
- Losing is possible from anywhere. A deal can die at any point. Modelling loss as “only from negotiation” is tidy and untrue.
- Lost reopens; won does not. “They came back next quarter” is routine, so Closed Lost has edges back into the funnel. Closed Won has none — a deal that turns out not to have been won is a correction, not a stage change, and freezing it means the revenue figure cannot be quietly rewritten.
- You cannot skip a rung. This is the rule people hit most, and the first one to relax if your team finds it fussy. It is a sample, not a prescription.
- The requirement is on the end state, not the whole funnel. You can leave the amount blank all the way through negotiation; you just cannot call it won.
The sample deals arrive spread across every stage, including ones the matrix would not let you reach in one move. That is the same rule as loading historical data: rules govern what happens next, not what already happened.
Seeing the pipeline as a board
Section titled “Seeing the pipeline as a board”Any table with a stage field gets a Board button above its records. The board lays the records out in columns, one per stage, in the order you put the stages in.
Dragging a card moves the record — it is the same edit as changing the stage on the record itself, so the same rules apply. Columns the card cannot move to are dimmed before you start dragging, which is a convenience rather than the rule: the server decides, and if a move is refused the card returns to where it was and tells you which stages it can reach. If somebody else changed the record while it was on your screen, the move is refused rather than overwriting them, and the board refreshes.
Cards can be moved with the keyboard as well as the mouse.
A few things about the layout are deliberate:
- End-state columns start collapsed. Closed Won and Closed Lost are usually the fullest columns and the least interesting day to day.
- Retired stages get an “Archived stages” column, and only when something is still in one. Cards can be dragged out of it, never in — the value is withdrawn, so the server would refuse.
- Nothing is ever hidden. A record whose stage is not in the list appears under “Unknown”, and one with no stage at all under “No stage”. A board that quietly left a deal out would be worse than one with an extra column, because you cannot notice something that isn’t shown.
- A computed stage is read-only. Its value follows from other fields, so there is nothing to drag; open the record and change the fields it reads.
The columns count the records the board has loaded. That is the whole table today, so the numbers are real totals.
Stage history and time in stage
Section titled “Stage history and time in stage”Once a field is designated, every move it makes is recorded: what it moved from, what it moved to, when, and which surface did it — a person in the app, a CSV import. You see it on the record itself, under Stage history.
Three things about what gets recorded are worth knowing, because they decide whether the numbers you build on this mean anything:
- Only genuine moves count. Saving a record without changing its stage is not a move, so a form that posts every field does not manufacture history. Setting a stage for the first time, and clearing it, both are.
- Only designated fields are tracked. An ordinary picklist has no history, which is the practical difference between “a dropdown” and “a pipeline”.
- History starts when you designate the field. There is no backfill — nothing knows when a record that already existed entered its current stage. Designate early if aging matters to you.
Each entry shows how long the record sat in the stage it just left. The oldest entry has no such figure, and shows none: its predecessor is unknown rather than zero, and reporting zero would drag every average you compute towards it.
Once a record reaches an end state, the panel also shows its cycle time — how long it took, from creation, to first get there. First, not most recent: a deal that is reopened and closed again keeps its original close as the measurement. Which values count as an end state comes from the option’s category, so renaming Closed Won to Booked changes nothing about the maths.
Deleting a record deletes its history along with it.
Working through the API or an AI agent
Section titled “Working through the API or an AI agent”The same vocabulary appears on the API and in get_table_schema for MCP
clients. Options come back in canonical order, each with its category
(open / terminal_success / terminal_failure) and, if it has been retired,
a flag saying it is no longer selectable.
Two contracts are worth knowing if you build against this:
- Unknown categories read as
open. The list can grow in a future release; a client written today must tolerate a category it has not heard of and treat it as open rather than failing. Classify with “is it open or not”, never by comparing against specific category names. - Writing into a retired value is rejected with a message naming the values you can choose. Re-submitting a record whose value has not changed is accepted — so a full update of an untouched record never fails because of a retirement that happened after it was created.
The machine lives at /tables/{tableId}/fields/{fieldId}/stage-config —
GET to read it (any table reader), PUT to create or replace it, DELETE to
undesignate. Unlike table constraints, it is freely editable: a matrix compiles
to no database object, so changing one rewrites a document rather than
rebuilding anything.
A refused move comes back as a 400 with code TRANSITION_NOT_ALLOWED and a
transition object holding field, from, to and allowedTargets. That
last one is what a board uses to grey out the columns a card cannot be dragged
to, so the refusal is prevented rather than merely reported.
A record’s timeline is at
/tables/{tableId}/items/{itemId}/stage-history — records:read, cursor
paginated, and scoped the same way the record itself is: a record you cannot see
returns 404, and a stage field you cannot read contributes no entries. It is
deliberately not the audit log, which is a separate permission and a different
kind of data.
Every write surface enforces the same rules — the API, the app, and CSV import (in import’s update mode, a row whose stage is unchanged is not a transition, so re-importing an export does not fail). The one thing that does not is restoring a backup, which replays saved state rather than performing writes.