# State Transitions

> Declare a state machine on a resource and saving through a transition becomes a typesafe, state-machine-safe backend function — declared once in TypeScript, enforced on every surface.

A resource can declare a **state machine** over one of its fields. Saving a record through a transition is then a fully typesafe, state-machine-safe backend function: declared once in TypeScript, compiled into the manifest, and enforced by the Rust core identically on the server, on Lambda, and in the in-browser WASM engine — **no per-resource server code**.

## Declare the machine

With the `resource()` DSL, transitions live in the options:

```ts title="resources/flights.ts"
export const Flight = resource("airports", "flight", {
  number: s.string({ maxLength: 8 }),
  status: s.string({ maxLength: 20 }),
}, {
  transitions: {
    field: "status",
    states: ["scheduled", "boarding", "departed", "cancelled"],
    initial: "scheduled",
    on: {
      board:  { from: "scheduled", to: "boarding" },
      depart: { from: "boarding",  to: "departed", set: { gate: null } },
      cancel: { from: ["scheduled", "boarding"], to: "cancelled" },
    },
  },
});
```

On a class entity, the same declaration is a `static transitions` member (statics are never columns, so it rides alongside the fields):

```ts title="entities/flights.ts"
export class Flight {
  number!: string & MaxLength<8>;
  status = "scheduled";   // the state field — the literal doubles as the default

  static transitions = {
    field: "status",
    states: ["scheduled", "boarding", "departed", "cancelled"],
    initial: "scheduled",
    on: {
      board:  { from: "scheduled", to: "boarding" },
      depart: { from: "boarding",  to: "departed", set: { gate: null } },
      cancel: { from: ["scheduled", "boarding"], to: "cancelled" },
    },
  };
}
```

## What compiles

- `states` become the field's `enum` — a select control in forms, and the `member` gate in the [validation machine](https://reactapistack.com/docs/validation.md).
- `initial` becomes the column default and the **only** state a record may be created in.
- The machine lands in `schema.rastack.json` and rides the OpenAPI contract as `x-rastack-transitions` — see [Manifest & OpenAPI](https://reactapistack.com/docs/manifest.md).

## Enforced on every surface

An update that moves the state field must follow a declared edge. If it doesn't, every surface rejects it the same way:

```text title="the engine's answer, everywhere"
400 Bad Request
status: no transition from "scheduled" to "departed"
```

- **Forms** — the record-level transition gate runs in `useForm` alongside the per-field gates.
- **Imports** — imported rows are creates, so `rastack import` rejects rows born mid-machine (any state but the initial one), with a row-located error.
- **The engine** — `rastack-api-core` enforces the machine on the server, on Lambda, and in the browser over WASM. There is no surface that skips it.

## The typed form surface

`useForm` exposes the machine, narrowed to the record's **current** state:

```tsx title="app/flights.tsx"
const form = useForm(Flight, { id });

form.state                 // "scheduled"
form.transitions           // [{ name: "board", to: "boarding", … }]
                           //   — only the edges legal from the current state
form.transition("board");  // typed: only names Flight declares compile

<AutoForm form={form} />   // renders one action button per available transition
```

> **Note:** Transition names are **literal types**: `form.transition("explode")` is a compile error, not a runtime 400.

## set patches — a declarative backend function

A matched edge's `set` patches are applied by the engine, server-side, atomically with the state change — `depart` above clears `gate` on every surface, whether the save came from a form, an import, or a raw API call. Declared once in TypeScript, executed by the engine: a backend function with no backend code.

> **Tip:** The transition gate is part of the constraint state machine — see [Validation & Data Import](https://reactapistack.com/docs/validation.md) for the per-field gates it sits alongside.

---

_Source: [https://reactapistack.com/docs/transitions](https://reactapistack.com/docs/transitions) · Full docs as one file: [llms-full.txt](https://reactapistack.com/llms-full.txt)_
