| name | status-transitions |
| description | Use when implementing state machines, status workflows, or status-based transitions in SAP CAP: @flow annotations, status-transition flows (Gamma status, production ready cds 9.5+), custom state machines, action-based status changes, or validating entry/exit conditions for entity states without writing boilerplate handler code.
|
| metadata | {"version":"1.1.0","keywords":["@flow","status transition","state machine","workflow","status","allowed transitions","Gamma status","cds 9.5"],"related":{"cds-modeling":"CDS entity with status field","service-handlers":"validate transitions in handlers","error-handling":"reject invalid state transitions"}} |
Status Transitions — CAP Best Practices
Primary reference: https://cap.cloud.sap/docs/releases/nov25
Status-Transition Flows: https://cap.cloud.sap/docs/guides/services/status-flows
Note: Status-Transition Flows are Gamma status as of CAP December 2025 — production ready in cds 9.5+
Option A: @flow annotations (Gamma status, cds 9.5+ — zero boilerplate)
Define state machines directly in CDS with @flow:
using { cuid, managed } from '@sap/cds/common';
type TravelStatus : String enum {
Open = 'O';
Accepted = 'A';
Canceled = 'C';
}
entity Travels : cuid, managed {
description : String;
status : TravelStatus default 'O';
totalCost : Decimal(9,2);
}
// Declare the state machine
annotate Travels with @flow.status: status actions {
acceptTravel @from: #Open @to: #Accepted; // Open → Accepted
rejectTravel @from: #Open @to: #Canceled; // Open → Canceled
reopenTravel @from: #Canceled @to: #Open; // Canceled → Open (optional)
}
CAP auto-generates:
- OData bound actions
acceptTravel, rejectTravel, reopenTravel
- Entry condition validation (
@from state check)
- State transition execution (
@to assignment)
- UI annotations for Fiori Elements Object Page buttons (auto-disabled when
@from not met)
Overriding generated logic for custom validation
module.exports = class TravelService extends cds.ApplicationService {
async init() {
this.before('acceptTravel', Travels, async (req) => {
const travel = await SELECT.one(Travels, req.params[0])
if (travel.totalCost <= 0) {
req.reject(422, 'Cannot accept a travel with zero cost')
}
})
return super.init()
}
}
Tracking transition history (restore previous state)
annotate Travels with @flow.status: status actions {
acceptTravel @from: #Open @to: #Accepted;
rejectTravel @from: #Open @to: #Canceled;
// $flow.previous = CAP tracks the previous state automatically
undoTransition @to: $flow.previous;
}
CAP adds the necessary data structure to the entity to track transition history.
Option B: Manual state machine (explicit handler — full control)
For complex workflows or when @flow isn't flexible enough:
type OrderStatus : String enum {
Draft = 'D';
Submitted = 'S';
Approved = 'A';
Rejected = 'R';
Shipped = 'X';
}
service OrderService {
entity Orders as projection on db.Orders;
action submitOrder() returns Boolean;
action approveOrder() returns Boolean;
action rejectOrder(reason: String) returns Boolean;
action shipOrder(trackingNr: String) returns Boolean;
}
const TRANSITIONS = {
submitOrder: { from: ['Draft'], to: 'Submitted' },
approveOrder: { from: ['Submitted'], to: 'Approved' },
rejectOrder: { from: ['Submitted'], to: 'Rejected' },
shipOrder: { from: ['Approved'], to: 'Shipped' },
}
module.exports = class OrderService extends cds.ApplicationService {
async init() {
this.on('submitOrder', Orders, (req) => this.transition('submitOrder', req))
this.on('approveOrder', Orders, (req) => this.transition('approveOrder', req))
this.on('rejectOrder', , .(, req, req.))
.(, , .(, req, req.))
.()
}
() {
{ , to } = [action]
{ } = req.[]
order = .(, )
(!order) req.(, , [])
(!.(order.)) {
req.(, , [order., action])
}
(, ).({ : to, ...extra })
}
}
Enum definition best practices for status fields
// Always use typed enums — not plain String
// Good: named values for readability + numeric safety
type InvoiceStatus : String enum {
Draft = 'D';
Pending = 'P';
Approved = 'A';
Rejected = 'R';
Paid = 'X';
}
// Annotate for Fiori criticality colours
annotate Invoices:status with @(
Common.ValueList: {
CollectionPath: 'InvoiceStatusVH',
Parameters: [{ LocalDataProperty: status, ValueListProperty: 'code' }]
},
UI.Hidden: false
);
Fiori Elements Object Page — auto-generated buttons
With @flow annotations, Fiori Elements automatically shows action buttons on the Object Page and disables them when the @from condition isn't met. No additional @UI.DataFieldForAction annotations required for the buttons, though you can customise placement:
annotate Travels with @UI.Facets: [{
$Type: 'UI.ReferenceFacet',
Target: '@UI.FieldGroup#Main'
}];
// Button placement (optional — auto-added if omitted)
annotate Travels with @UI.LineItem: [
{ $Type: 'UI.DataFieldForAction', Action: 'TravelService.acceptTravel', Label: 'Accept' },
{ $Type: 'UI.DataFieldForAction', Action: 'TravelService.rejectTravel', Label: 'Reject' },
];
Common mistakes to avoid
- ❌ Using plain
String for status instead of a typed enum — lose value help and criticality
- ❌ Allowing status changes via
PATCH on the status field directly — always go through actions
- ❌ Not defining
@from guards — any action can be called in any state
- ❌ Mixing
@flow and manual action handlers for the same entity — use one approach
- ❌ Using @flow on cds < 9.5 — Gamma status requires cds 9.5+
- ❌ Not logging status transitions — add audit via
@cap-js/change-tracking on the status field
CAP Java — additional setup
In CAP Node.js, @flow support is built-in. For CAP Java, add the Maven dependency:
<dependency>
<groupId>com.sap.cds</groupId>
<artifactId>cds-feature-flow</artifactId>
<scope>runtime</scope>
</dependency>
Current limitations
- Draft-enabled entities: all actions are disabled when the entity is in draft state — status transitions can only be performed on active entities.
- CRUD/DRAFT operations cannot be restricted by status-transition flows — only bound actions can be flow-controlled.