SuperAPI AI Directory

status-transitions

Coding & development

View repository

Updated 10-16 11:48

About this skill

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

```js
// srv/travel-service.js
module.exports = class TravelService extends cds.ApplicationService {
  async init() {
    // Override just the part you need — before the generated handler
    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;
}
// Define valid transitions
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',  Orders, (req) => this.transition('rejectOrder',  req, req.data))
    this.on('shipOrder',    Orders, (req) => this.transition('shipOrder',    req, req.data))
    return super.init()
  }

  async transition(action, req, extra = {}) {
    const { from, to } = TRANSITIONS[action]
    const { ID } = req.params[0]

    const order = await SELECT.one(Orders, ID)
    if (!order) req.reject(404, 'ORDER_NOT_FOUND', [ID])
    if (!from.includes(order.status)) {
      req.reject(409, 'INVALID_TRANSITION', [order.status, action])
    }

    await UPDATE(Orders, ID).with({ status: to, ...extra })
    return true
  }
}

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:

<!-- srv/pom.xml -->
<dependency>
  <groupId>com.sap.cds</groupId>
  <artifactId>cds-feature-flow</artifactId>
  <scope>runtime</scope>
</dependency>

Current limitations

  1. Draft-enabled entities: all actions are disabled when the entity is in draft state — status transitions can only be performed on active entities.
  2. CRUD/DRAFT operations cannot be restricted by status-transition flows — only bound actions can be flow-controlled.