DJ Adams

A simple exploration of status transition flows in CAP

In this post I explore the new Status-Transition Flows in CAP with a simple example.

The November 2025 release of CAP heralded a beta version of Status-Transition Flows, moving us up yet another gear in the journey towards declarative nirvana.

Background

I wanted to try out this new significant feature, one which embodies and celebrates one of the key reasons to use CAP - the code is in the framework, not outside of it. In the November release notes there's a brief glimpse of what this declarative approach looks like:

annotate Travels with @flow.status: Status actions {
  rejectTravel    @from: #Open  @to: #Canceled;
  acceptTravel    @from: #Open  @to: #Accepted;
  deductDiscount  @from: #Open;
};

Of course, this is only part of how things are set up; following the link at the end of this section of the release notes leads us to the relevant Capire section within the Providing Services topic, where we can see some of the rest of the CDS model (domain model and service definition specifically) that goes to make up the sample.

I wanted to write my own example, complete but also as simple as possible, so I could then stare at it for a while to let things sink in. Here's what I came up with.

double toggle switch (Image courtesy of Wikimedia Commons)

Modelling an on/off switch

After initialising a new CAP project (with cds init), I just created a services.cds file1 with the following content:

context qmacro {

  type Status : String enum {
    Up;
    Down;
  }

  entity Switches {
    key ID     : Integer;
        status : Status default #Down;
  }
}

service SwitchService {

  entity Switches as projection on qmacro.Switches

    actions {
      action flipUp();
      action flipDown();
    };


  annotate Switches with @flow.status: status actions {
    flipUp               @from       : #Down  @to: #Up;
    flipDown             @from       : #Up    @to: #Down;
  };
}

Understanding the definitions

Recently I've been looking more closely at CDS modelling in general and CDL in particular, partly in the context of creating the exercise content for the Hands-on with CAP CDS workshop which I gave at UKISUG Connect at the start of this month. And I have come to value taking my time to understand the nuances of how models are expressed. Here are a few points of interest relating to the declarations here:

With regards to the data model:

With regards to the service definition:

Finally there's the annotation:

Digging in to the annotation

The annotation is quite involved:

annotate <target> with <annotation> : <info> actions { ... }

and indeed the construction used is rather unusual:

What we have here is a sequence of two annotations in one. The single annotation expression above could just as readily be expressed like this2:

annotate Switches with @flow.status: status;

annotate Switches actions {
  flipUp    @from: #Down  @to: #Up;
  flipDown  @from: #Up    @to: #Down;
};

Separating these out like this makes it a bit easier for me to grok what's going on.

First, we're blessing the Switches entity with the Status-Transition Flows ability; at this level, we must specify the element to be used for the status. It's the status element that's important, and we do that here by targeting the entire entity with the annotation and then specifying the element as a secondary piece of information. We could have annotated the element directly, like this:

context qmacro {

  // ...

  entity Switches {
    key ID     : Integer;

        @flow.status
        status : Status default #Down;
  }
}

service SwitchService {

  // ...

  annotate Switches actions {
    flipUp    @from: #Down  @to: #Up;
    flipDown  @from: #Up    @to: #Down;
  };

}

but, we'd have needed to additionally, explicitly and manually specify the @readonly annotation as directed:

entity Switches {
  key ID     : Integer;

      @readonly
      @flow.status
      status : Status default #Down;
}

In the current state of how we make these declarations, specifying the flow status annotation at the entity level, and in the service layer, nearer to the related actions, makes more sense to me.

In case you're wondering, the original entity level @flow.status annotation does indeed also cause a @readonly annotation to be added; here's the relevant section of the CSN that's generated:

definitions:
  SwitchService.Switches:
    kind: entity
    "@flow.status": { "=": status }
    projection: { from: { ref: [qmacro.Switches] } }
    elements:
      ID: { key: true, type: cds.Integer }
      status:
        type: qmacro.Status
        default: { "#": Down, val: Down }
        "@flow.status": true
        "@readonly": true

And, what of the actions themselves?

Looking at the implementation for the actions

We are used to having the CAP server provide a complete out-of-the-box CRUD handler experience for our services, on an entity by entity basis. We are also used to having to write our own handlers for custom "orthogonal" offerings such as actions and functions, as, almost by definition, they could be anything and are unguessable.

And so here we are now with a couple of actions we've declared:

// ...

service SwitchService {

  entity Switches as projection on qmacro.Switches

    actions {
      action flipUp();
      action flipDown();
    };

    // ...

and annotated:

  // ...

  annotate Switches actions {
    flipUp    @from: #Down  @to: #Up;
    flipDown  @from: #Up    @to: #Down;
  };
}

Here's what the implementation looks like, say in a corresponding services.js file:

Yep, that's right. There is no implementation required, and no corresponding implementation file needed - that's sort of the whole point!

With the declarative Status-Transition Flows approach, we've declared everything we need with the annotations, describing what each action should do with respect to the status; what starting status requirements there are and what target statuses there can be. Here:

Of course, there are far more flexible and involved possibilities here, which are nicely described in the appropriate section.

Kicking the tyres

Now I have my model and service definition, and the appropriate annotations, what does it feel like, how does one interact with and experience it?

Simply from an HTTP level, I'll give it a go. One can imagine how this then translates to calls being made from a frontend somewhere too, with buttons and other widgets in the UI enabled or disabled (or even hidden) according to the current status.

I'll start the server with cds watch.

Creating my first switch

OK, are there any existing switch entities?

; curl \
  --silent \
  --url 'localhost:4004/odata/v4/switch/Switches' \
  | jq
{
  "@odata.context": "$metadata#Switches",
  "value": []
}

No. So I'll create one:

; curl \
  --header 'Content-Type: application/json' \
  --data '{"ID":1}' \
  --silent \
  --url 'localhost:4004/odata/v4/switch/Switches' \
  | jq
{
  "@odata.context": "$metadata#Switches/$entity",
  "ID": 1,
  "status": "Down"
}

It has the default status of Down, as expected.

Trying to flip the switch the wrong way

The switch is down, so I want to see if I can invoke the flipDown action3:

; curl \
  --request POST \
  --include \
  --url 'localhost:4004/odata/v4/switch/Switches/1/flipDown'
HTTP/1.1 409 Conflict
X-Powered-By: Express
OData-Version: 4.0
Content-Type: application/json; charset=utf-8
Content-Length: 151

{
  "error": {
    "message": "Action \"flipDown\" requires \"status\" to be \"[\"Up\"]\".",
    "code": "INVALID_FLOW_TRANSITION_SINGLE",
    "@Common.numericSeverity": 4
  }
}

Nope! Plus, the HTTP status code 409 is nicely appropriate.

Flipping the switch the right way

So how about flipping the switch from down to up?

; curl \
  --request POST \
  --include \
  --url 'localhost:4004/odata/v4/switch/Switches/1/flipUp'
HTTP/1.1 204 No Content
X-Powered-By: Express
OData-Version: 4.0

That seemed to work, but I'll check anyway:

; curl \
  --silent \
  --url 'localhost:4004/odata/v4/switch/Switches/1' \
  | jq
{
  "@odata.context": "$metadata#Switches/$entity",
  "ID": 1,
  "status": "Up"
}

Excellent!

Wrapping up

Of course, there's a lot more that this new Status-Transition Flows feature offers, but for now, I'm glad I took a first look with this simple example.

For further explorations and explanations, see Simon Engel's great session Status Transition Flows in CAP from Devtoberfest earlier this year, as well as the coverage in Capire.

And remember, this is a beta feature right now, so a great time to try it out for yourself.

Footnotes

  1. The name of this file is significant, it's one of the two file items (the rest are directory items) in the "CDS roots", as illustrated:

    ; cds env roots
    [
      "db/",
      "srv/",
      "app/",
      "schema",
      "services"
    ]

    This means that it's a default ("well-known") location for CDS model definitions.

  2. What's really going to blow your mind is that annotate is really just a shortcut variant of extend.

  3. I use --include to have the response headers emitted, but have only included some of them in the output here, to keep things brief.