Serving SAP Fiori UIs
CAP provides out-of-the-box support for SAP Fiori elements. This guide explains how to add one or more SAP Fiori elements apps to a CAP project, how to add SAP Fiori elements annotations to respective service definitions, and more. In the following sections, when mentioning Fiori, we always mean SAP Fiori elements.
Learn more about developing SAP Fiori elements and OData V4 (since 1.84.)
Getting Started
Using Fiori Previews
Fiori Preview
For entities exposed via OData V4 there is a Fiori preview link on the index page. It dynamically serves an SAP Fiori elements list page that allows you to quickly see the effect of annotation changes without having to create a UI application first.
Not for production
The preview is meant for quick tests and iterations during development, but not for production use, and hence automatically disabled in production. To also enable it in cloud deployments, for test or demo purposes maybe, set cds.fiori.preview:true for Node.js apps, or cds.index-page.enabled:true for Java
Adding Fiori Apps
As showcased in cap/samples, SAP Fiori apps should be added as sub folders to the app/ of a CAP project. Each sub folder constitutes an individual SAP Fiori application, with local annotations, manifest.json, etc. So, a typical folder layout would look like this:
| Folder/Sub Folder | Description |
|---|---|
app/ | All SAP Fiori apps should go in here |
browse/ | SAP Fiori app for end users |
orders/ | SAP Fiori app for order management |
admin/ | SAP Fiori app for admins |
index.html | For sandbox tests |
srv/ | All services |
db/ | Domain models, and db stuff |
Tip
Links to Fiori applications created in the app/ folder are automatically added to the index page of your CAP application for local development.
SAP Fiori Tools
The SAP Fiori tools provide advanced support for adding SAP Fiori apps to existing CAP projects as well as a wealth of productivity tools, for example for adding SAP Fiori annotations, or graphical modeling and editing. They can be used locally in Visual Studio Code (VS Code) or in SAP Business Application Studio.
OData Annotations Plugin
The SAP CDS language support plugin includes a plugin. It helps you adding and editing OData annotations in CDS syntax in VS Code by providing the following features:
- Code completion
- Validation against the OData vocabularies and project metadata
- Navigation to the referenced annotations
- Quick view of vocabulary information
- Internationalization support
These assisting features are provided for OData annotations in CDS syntax and can't be used yet for the core data services common annotations.
The @sap/ux-cds-odata-language-server-extension module doesn't require any manual installation. The latest version is fetched by default from npmjs.com as indicated in the user preference setting CDS > Contributions: Registry.
Learn more about the CDS extension for VS Code.
Fiori Annotations
SAP Fiori elements apps are generic front ends, which construct and render the pages and controls based on annotated metadata documents. The annotations provide semantic annotations used to render such content, for example:
annotate CatalogService.Books with @(
UI: {
SelectionFields: [ ID, price, currency_code ],
LineItem: [
{Value: title},
{Value: author, Label:'{i18n>Author}'},
{Value: genre.name},
{Value: price},
{Value: currency.symbol, Label:' '},
]
}
);Find this source and many more in capire/bookstore.Learn more about OData Annotations in CDS.
Where to Put Them?
While CDS in principle allows you to add such annotations everywhere in your models, we recommend putting them in separate .cds files placed in your ./app/* folders, for example, as follows.
./app #> all your Fiori annotations should go here, for example:
./admin
fiori-service.cds #> annotating ../srv/admin-service.cds
./browse
fiori-service.cds #> annotating ../srv/cat-service.cds
services.cds #> imports ./admin/fiori-service and ./browse/fiori-service
./srv #> all service definitions should stay clean in here:
admin-service.cds
cat-service.cds
...See this also in capire/bookstore.
Reasoning: This recommendation essentially follows the best practices and guiding principles of Conceptual Modeling and Separation of Concerns.
Prefer @title and @description
Influenced by the JSON Schema, CDS supports the common annotations @title and @description, which are mapped to corresponding OData annotations as follows:
| CDS | JSON Schema | OData |
|---|---|---|
@title | title | @Common.Label |
@description | description | @Core.Description |
We recommend preferring these annotations over the OData ones in protocol-agnostic data models and service models, for example:
annotate my.Books with { //...
title @title: 'Book Title';
author @title: 'Author ID';
}Prefer @readonly, @mandatory, ...
CDS supports @readonly as a common annotation, which translates to respective OData annotations from the @Capabilities vocabulary. We recommend using the former for reasons of conciseness and comprehensibility as shown in this example:
@readonly entity Foo { // entity-level
@readonly foo : String // element-level
}is equivalent to:
entity Foo @(Capabilities:{
// entity-level
InsertRestrictions.Insertable: false,
UpdateRestrictions.Updatable: false,
DeleteRestrictions.Deletable: false
}) {
// element-level
@Core.Computed foo : String
}Similar recommendations apply to @mandatory and others → see Common Annotations.
Simple Value Helps
In addition to supporting the standard @Common.ValueList annotations as defined in the OData Vocabularies, CAP provides convenient support for Value Helps.
@cds.odata.valuelist
Simply add the @cds.odata.valuelist annotation to an entity, and all managed associations targeting this entity will automatically receive Value Lists in SAP Fiori clients. For example:
@cds.odata.valuelist
entity Currencies { key code ... }service BookshopService {
entity Books { //...
currency : Association to Currencies;
}
}This would be expanded by the compiler to the following OData annotations, in the EDMX documents generated for Fiori clients:
<Annotations Target="AdminService.Books/currency_code">
<Annotation Term="Common.ValueList">
<Record Type="Common.ValueListType">
<PropertyValue Property="CollectionPath" String="Currencies"/>
<PropertyValue Property="Label" String="Currency"/>
<PropertyValue Property="Parameters">
<Collection>
<Record Type="Common.ValueListParameterInOut">
<PropertyValue Property="ValueListProperty" String="code"/>
<PropertyValue Property="LocalDataProperty" PropertyPath="currency_code"/>
</Record>
<Record Type="Common.ValueListParameterDisplayOnly">
<PropertyValue Property="ValueListProperty" String="name"/>
</Record>
</Collection>
</PropertyValue>
</Record>
</Annotation>
</Annotations>@sap/cds/common
The reuse types in @sap/cds/common already have this added to base types and entities, so all uses automatically benefit from this. This is an effective excerpt of respective definitions in @sap/cds/common:
type Currencies : Association to sap.common.Currencies;context sap.common {
entity Currencies : CodeList {...};
entity CodeList { name : localized String; ... }
}annotate sap.common.CodeList with @(
UI.Identification: [name],
cds.odata.valuelist,
);In effect, usages of @sap/cds/common stay clean of any pollution, for example:
using { Currency } from '@sap/cds/common';
entity Books { //...
currency : Currency;
}Find this also in capire/bookstore.
With that, all UIs on all services exposing Books will automatically receive Value Help for currencies. You can also benefit from that when deriving your project-specific code list entities from sap.common.CodeList.
Fiori Draft Support
Draft Support
SAP Fiori uses Drafts to enable users to save their progress while editing data and continue later on without losing changes. Drafts are stored on the server and can be accessed from different devices and locations, providing flexibility and convenience for users. CAP provides out-of-the-box support for drafts, making it easy to implement this functionality in your applications.
This documentation focuses on CAP only
For general information on the user experience and the technical details of drafts in SAP Fiori, please consult respective SAP Fiori documentation, such as the SAP Fiori Design Guidelines, and the SAP UI5 documentation.
Draft-Enabled Entities
All you need to do to serve an entity with draft support enabled is to annotate it with @odata.draft.enabled. For example, as we do in the capire/xtravels sample:
annotate TravelService.Travels with @odata.draft.enabled;Behind the scenes, CAP takes care of all the rest. Most importantly, it adds a new .drafts entity, next to the active entity, and with the same elements, used to store draft data. Think of it like a shadow entity, defined like this:
entity TravelService.Travels.drafts : TravelService.Travels { ... }We can access the definition of this entity from the model at runtime to, for example, add custom handlers to draft events, or to access draft data in our code. For example, from within a service implementation in CAP Node.js, we can use the .drafts reference as a shortcut to access the draft entity:
const { Travels } = this.entities
SELECT.from (Travels) //> queries active data
SELECT.from (Travels.drafts) //> queries draft dataDraft Choreography
With @odata.draft.enabled entities in place, CAP automatically serves the Fiori draft choreography as illustrated in the following diagram:
In essence, the draft choreography defines the following flows:
- creating drafts for new active entities, or for editing existing ones
- filling in draft data through a series of PATCH events
- saving the draft back to the active entity, or discarding it
Drafts are isolated from any active data until they are saved/activated, when discarded, they are removed, as if they never existed – with draft locks as the only exception, to prevent conflicting changes.
Draft Locks
Whenever a draft is created to edit an active entity, this active entity is locked for any operation that could result in conflicting changes. In particular:
- No other draft can be created for the same active instance.
- No direct updates or deletes to the active instance are allowed.
The lock is released automatically when the draft is either saved/activated, or discarded. It can also be manually reclaimed by other users after a certain period of inactivity, which is 15 minutes by default, but can be configured via cds.fiori.draft_lock_timeout: 1h for CAP Node.js and cds.drafts.cancellationTimeout: 1h for CAP Java, respectively. See draft lock configuration for Node.js or Java.
Draft locks are not applied when creating drafts for new entities, as there is no active entity to be locked in this case.
Requests to Drafts
The HTTP requests sent from Fiori clients that deal with drafts are as follows:
POST /Foo/draftNew //> NEW
POST /Foo(ID,IsActiveEntity=true)/draftEdit //> EDIT
GET /Foo(ID,IsActiveEntity=false) //> READ
PATCH /Foo(ID,IsActiveEntity=false) {...} //> PATCH
POST /Foo(ID,IsActiveEntity=false)/draftActivate //> SAVE
DELETE /Foo(ID,IsActiveEntity=false) //> DISCARD2
3
4
5
6
The key parameter IsActiveEntity=false is used to address draft data – with the exceptions of draftNew, which is an unbound action, and draftEdit for semantic reasons.
draftNew actions are used Since @sap/cds v10 and Since CAP Java v5
Before, regular POST /Foo requests without an IsActiveEntity parameter were used to create new drafts, which always created ambiguities with requests to active data. Can be disabled with cds.fiori.draft_new_action: false.
Full HTTP requests ...
The requests above are shown in an abbreviated form for clarity. The actual HTTP requests include the service path, content-type headers and JSON bodies as shown below.
POST /odata/v4/TravelService/Travels/draftNew
Content-Type: application/jsonPOST /odata/v4/TravelService/Travels(ID=a11fb6f1-36ab-46ec-b00c-d379031e817a,IsActiveEntity=true)/draftEdit
Content-Type: application/jsonPATCH /odata/v4/TravelService/Travels(ID=a11fb6f1-36ab-46ec-b00c-d379031e817a,IsActiveEntity=false)
Content-Type: application/json
{ ... }... and so forth.
Requests to Active Data
Add IsActiveEntity=true as a key parameter to your requests to address active data directly, bypassing potentially existing drafts, for example:
POST /Books { IsActiveEntity:true, ... } //> CREATE
PATCH /Books(ID=201,IsActiveEntity=true) {...} //> UPDATE
DELETE /Books(ID=201,IsActiveEntity=true) //> DELETE
GET /Books(ID=201,IsActiveEntity=true) //> READ2
3
4
Available for CAP Node.js Since @sap/cds v10
While this was always possible in CAP Java before, it's available for CAP Node.js in the same way by default since v10. Can be disabled with cds.fiori.bypass_draft: false, which prevents bypassing the draft flow for CREATE and UPDATE operations entirely.
Draft-agnostic Requests
Going one step further, we assume IsActiveEntity=true by default, so that clients which don't know anything about drafts, or don't want to deal with them, can simply ignore any draft-specific requests and parameters:
POST /Foo //> CREATE
GET /Foo(ID) //> READ
PATCH /Foo(ID) {...} //> UPDATE
DELETE /Foo(ID) //> DELETE2
3
4
Available for CAP Node.js Since @sap/cds v10 – not yet for CAP Java
Draft-agnostic requests as above assume IsActiveEntity=true by default for all requests that don't explicitly specify it. Doing so was possible in CAP Node.js, but not in CAP Java, as we are (still) bound by the Olingo library there. Thus, for CAP Java, you need to explicitly add IsActiveEntity=true as key parameters to address active data
Draft locks still apply
Directly updating an active entity does not bypass draft locks. If an existing draft locks the entity, direct updates are blocked to prevent lost update situations.
Programmatic Access
You can also access draft data programmatically from custom code in JavaScript or Java.
In CAP Java, add IsActiveEntity as a key parameter to your queries (learn more):
Select.from(FOO).where(o -> o.ID().eq(201); //> reads active data
Select.from(FOO).where(o -> o.ID().eq(201) .and( //> reads draft data
o.IsActiveEntity().eq(false))
);In CAP Node.js, use the Foo.drafts references to access draft data:
const { Foo } = this.entities
SELECT.from (Foo, 201) // reads active data only
SELECT.from (Foo.drafts, 201) // reads draft data, if existsOr even better, prefer using req.subject which automatically resolves to the correct entity instance, active or draft, based on the current request context. For example, in a custom action handlers, that should be triggered for both active and draft data:
this.on ('approveTravel', req => UPDATE (req.subject) .with ({ status: 'A' }))
this.on ('rejectTravel', req => UPDATE (req.subject) .with ({ status: 'X' }))Draft Input Validation
Validating Drafts
During draft phase, i.e., on PATCH requests to draft data, all @asserts are validated, and respective error messages are returned to the client. Yet, in contrast to active entities, the draft is still created/updated, even with invalid data, so that users can correct it later on without losing their progress.
Custom Handlers for Draft Events
You can add custom handlers to draft events by referring to the draft-specific events and/or .drafts entities, for example like that in CAP Node.js (learn more):
const { Foo } = this.entities
this.before ('NEW', Foo.drafts, ...)
this.before ('EDIT', Foo, ...) //> note: refers to active entity
this.before ('PATCH', Foo.drafts, ...)
this.before ('SAVE', Foo.drafts, ...)
this.before ('DISCARD', Foo.drafts, ...)2
3
4
5
6
Similar in CAP Java (learn more):
@Before (event = DraftService.EVENT_DRAFT_CREATE)
@Before (event = DraftService.EVENT_DRAFT_EDIT)
@Before (event = DraftService.EVENT_DRAFT_PATCH)
@Before (event = DraftService.EVENT_DRAFT_SAVE)
@Before (event = DraftService.EVENT_DRAFT_CANCEL)2
3
4
5
Validation on Active Entities
Finally upon saving a draft, all validations for active entities are executed as usual, with invalid data being rejected, so that only valid data gets activated. This includes all constraints like @assert and @readonly, as well as all custom handlers registered to active entity events, for example:
const { Foo } = this.entities
this.before ([ 'CREATE', 'UPDATE' ], Foo, req => {/* validate all */})Validate on active entities, not only on drafts
Validations on draft entities are not sufficient, as active entities can be updated directly, bypassing drafts. Thus, make sure to always perform all necessary validations on active entities, not only on drafts. Note also, that updates to active entities could be partial, for example only updating an individual OrderItem within an Order through navigation like PATCH /Orders(1)/Items(3), so make sure to cover such cases as well in your validation logic.
Persistent Messages
While in draft state, error messages are automatically persisted, so that they remain visible, even after editing other fields, or navigating away from the page and returning later on.
CAP automatically generates corresponding side-effect annotations in the EDMX to instruct Fiori clients to fetch state messages after every PATCH request.
You can override generated side-effect annotations per entity, for example:
annotate MyService.Foo with @(
Common.SideEffects #alwaysFetchMessages: false
);Available Since CAP Java v3.8 and Since CAP Node.js v9.1
Can be disabled with cds.fiori.draft_messages: false.
Draft for Localized Data
Annotate the underlying base entity in the base model with @fiori.draft.enabled to also support drafts for localized data:
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;Background
SAP Fiori drafts required single keys of type UUID, which isn't the case case for .texts entities, generated for localized data. The @fiori.draft.enabled annotation tells the compiler to add an additional technical primary key element named ID_texts.

See it live in capire/bookstore.
Fiori Tree Views
Following the same principle of convenience as for Value Helps, CAP provides a shortcut annotation to define hierarchies on entities with recursive associations, which are then rendered as Tree Views in SAP Fiori clients.
Recursive Associations
Hierarchies are most commonly parent-child structures created via recursive associations. For example, in a the capire/bookshop sample, we have the Genres entity with a recursive association parent to itself:
entity Genres : cuid { //...
parent : Association to Genres;
}The @hierarchy Annotation
All we have to do to get a Tree View in SAP Fiori clients is to annotate the entity with @hierarchy, for example as we did in the Fiori Annotations of the capire/bookstore sample:
annotate AdminService.Genres with @hierarchy;If there are multiple associations that could be used as parent association, you can specify the one to be used with the value of the @hierarchy annotation, for example:
annotate AdminService.Genres with @hierarchy: parent;Under the hood...
The @hierarchy annotation is a convenient shortcut for the following Fiori-level annotate and extend statements that you would have to write manually otherwise.
// declare a hierarchy with the qualifier "GenresHierarchy"
annotate AdminService.Genres with @Aggregation.RecursiveHierarchy #GenresHierarchy: {
NodeProperty : ID, // identifies a node, usually the key
ParentNavigationProperty : parent // navigates to a node's parent
};
extend AdminService.Genres with @(
// The computed properties expected by Fiori to be present in hierarchy entities
Hierarchy.RecursiveHierarchy #GenresHierarchy: {
LimitedDescendantCount : LimitedDescendantCount,
DistanceFromRoot : DistanceFromRoot,
DrillState : DrillState,
LimitedRank : LimitedRank
},
// Disallow filtering on these properties from Fiori UIs
Capabilities.FilterRestrictions.NonFilterableProperties: [
'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank'
],
// Disallow sorting on these properties from Fiori UIs
Capabilities.SortRestrictions.NonSortableProperties: [
'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank'
],
) columns { // Ensure we can query these columns from the database
null as LimitedDescendantCount : Int16,
null as DistanceFromRoot : Int16,
null as DrillState : String,
null as LimitedRank : Int16
};Note: When naming the hierarchy qualifier, use the following pattern:
<entity name in service>Hierarchy
UI5 manifest Configuration
In addition, you need to configure the TreeTable in UI5's manifest.json file:
"sap.ui5": { ...
"routing": { ...
"targets": { ...
"GenresList": { ...
"options": {
"settings": { ...
"controlConfiguration": {
"@com.sap.vocabularies.UI.v1.LineItem": {
"tableSettings": {
"hierarchyQualifier": "GenresHierarchy",
"type": "TreeTable"
}
}
}
}
}
},
},
},Note: construct the
hierarchyQualifierwith the following pattern:<entity name in service>Hierarchy
You can now start the server with cds watch and see the hierarchical tree view in action in the Browse Genres app.

The compiler automatically expands the shortcut annotation @hierarchy to the following annotate and extend statements.
Cache Control in Java
CAP provides the option to set a Cache-Control header with a max-age directive to indicate that a response remains fresh until n seconds after it was generated . In the CDS model, this can be done using the @http.CacheControl: {maxAge: <seconds>} annotation on stream properties. The header indicates that caches can store the response and reuse it for subsequent requests while it's fresh. The max-age (in seconds) specifies the maximum age of the content before it becomes stale.
Elapsed time since the response was generated
The max-age is the elapsed time since the response was generated on the origin server. It's not related to when the response was received.
Only Java
Cache Control feature is currently supported on the Java runtime only.
Role-based Visibility
In addition to adding restrictions on services, entities, and actions/functions, there are use cases where you only want to hide certain parts of the UI for specific users. This is possible by using the respective UI annotations like @UI.Hidden or @UI.CreateHidden in conjunction with $edmJson pointing to a singleton.
First, you define the singleton in your service and annotate it with @cds.persistence.skip so that no database artefact is created:
@odata.singleton @cds.persistence.skip
entity Configuration {
key ID: String;
isAdmin : Boolean;
}A key is technically not required, but without it some consumers might run into problems.
Then define an on handler for serving the request:
srv.on('READ', 'Configuration', async req => {
req.reply({
isAdmin: req.user.is('admin') //admin is the role, which for example is also used in @requires annotation
});
});Finally, refer to the singleton in the annotation by using a dynamic expression:
annotate service.Books with @(
UI.CreateHidden : { $edmJson: {$Not: { $Path: '/CatalogService.EntityContainer/Configuration/isAdmin'} } },
UI.UpdateHidden : { $edmJson: {$Not: { $Path: '/CatalogService.EntityContainer/Configuration/isAdmin'} } },
);The Entity Container is OData specific and refers to the $metadata of the OData service in which all accessible entities are located within the Entity Container.
SAP Fiori elements also allows to not include it in the path
annotate service.Books with @(
UI.CreateHidden : { $edmJson: {$Not: { $Path: '/Configuration/isAdmin'} } },
UI.UpdateHidden : { $edmJson: {$Not: { $Path: '/Configuration/isAdmin'} } },
);