Skip to content

Move a record through states

A lifecycle gives a table a state column and the moves between its states. Use it for an editorial workflow, an approval, or any record that is not only created and edited.

table posts "Posts" {
display: title
title text required "Title"
body markdown required "Content"
lifecycle status "Status" {
state draft default "Draft"
state review "In review"
state published public "Published"
state archived gone "Archived"
transition submit draft -> review "Send for review" {
access {
author where author = :user.person_id
editor
}
}
transition publish review -> published "Publish" {
access: editor
set published_at = now
after -> logic/post-published.ts
}
transition archive published -> archived "Archive" {
access: editor
}
}
}

The state column is engine-owned. It never appears in a form, and a request can never set it. A state changes only through a transition.

default names the state a new row starts in. public marks a state whose rows a public page can read, and status.public is then usable in a condition.

A transition carries its own access:. The author can send a post for review; only an editor can publish it.

A button is absent when the reader cannot use it, and also when the record is not in a state the transition applies from. There is no disabled button that explains itself when you press it.

A transition is an action of the page rather than of a region. Which row the page is about already has one answer, so the transition does not restate it.

The source state and the reader’s row filter are both conditions of the update. If somebody left a page open and the record moved on in the meantime, their button does nothing and says so.

after -> runs a script after the move is committed. It sees the record after the move and receives the previous row as well, so it can say what changed.

transition publish review -> published "Publish" {
access: editor
after -> logic/post-published.ts
}

A transition’s set values, the table’s own checks over the moved row, and a before hook all run in one transaction with the move. The after hook runs after the commit, so it cannot roll the move back and cannot make a successful move look failed.

A table’s insert and update hooks do not run on a transition. A move is not an edit.

Status: the gone modifier is accepted and is not yet enforced. An archived record is hidden today because the app’s own pages filter on status.public. The engine does not yet return 410 for one or exclude it from listings by itself.